diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c63dc43..cb37e12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,8 +34,7 @@ jobs: run: pnpm install --frozen-lockfile # the CLAP prober, the LV2 measurer, their interposer and the fixture plugins the tests qualify; - # deps/ also holds openmixer's copy of the measurer, which the parity test runs beside ours, and - # omx-clap-core's LV2 feature provider at the packages' floor, built for the measurer to link + # deps/ also holds omx-clap-core's LV2 feature provider at the packages' floor, built for the measurer to link # asan: the measurer under AddressSanitizer, which the leak check runs - name: Build the native tools run: | diff --git a/BUILDING.md b/BUILDING.md index 17d29f0..4ca4839 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -34,25 +34,21 @@ Its host features are omx-clap-core's LV2 feature provider, the static `libomx-c header: `pkg-config omx-clap-lv2` (`omx-clap-lv2-devel`, `libomx-clap-lv2-dev`), else `make deps` fetches omx-clap-core at the packages' floor, builds the archive against the fetched CLAP headers and installs it into `native/deps/omx-clap-lv2`. -That header is also what openmixer's copy of the LV2 measurer reads (kept in -`native/parity/openmixer` as it was at the commit it was taken from, each file's blob id in `BLOBS`), -so the build carries that copy, `native/build/parity/lv2_measure`, beside ours: ``` make -C native deps all # native/build/clap-qualify, lv2_measure, rt_interpose.so, fixtures/ ``` -The native tests run the build; without it they skip, except on CI, where a missing build fails. The -parity test runs both LV2 measurers on the fixture bundles and wants the same bytes from each, once -the three things only ours prints are taken out of its line: the `host` object, the -`featureOptions` object and the soak's `maxRmsDbfs` (the soak case lifts the sweep's bound, so both -sweep the same range). +The native tests run the build; without it they skip, except on CI, where a missing build fails. `make -C native asan` builds the measurer under AddressSanitizer, `native/build/asan/lv2_measure`, which the leak check runs on every way out of it (a measurement, a soak, a plugin that will not instantiate). It is not part of `all`, so a package build needs no libasan. -`make -C native install DESTDIR=... LIBEXECDIR=...` installs the prober, the measurer and the -interposer (not the fixtures): exactly `NATIVE_TOOLS` in `src/qualify-declarations.ts`. The command +`make -C native install DESTDIR=... LIBEXECDIR=...` installs the prober, the measurer, the +interposer and the LV2 host probe (not the fixtures): exactly `NATIVE_TOOLS` in +`src/qualify-declarations.ts`. The host probe (`lv2_host_probe [uri]`) loads a worker plugin +through lilv and drives it as a host does, so a console that exports an LV2 plugin tests that +plugin's host-facing half with it. The command and `tools/lv2-measure.mjs` look for each by its setting (`OMX_CLAP_QUALIFY`, `OMX_LV2_MEASURE`, `OMX_RT_INTERPOSE`), then in `native/build/`, then in the directories the packages install into (`NATIVE_TOOL_DIRS`). An installed package has no `native/build/`; in a checkout, its own build wins diff --git a/native/Makefile b/native/Makefile index 6128e1e..35ebf74 100644 --- a/native/Makefile +++ b/native/Makefile @@ -14,9 +14,8 @@ # make -C native deps fetch the CLAP and plugin-hostd headers at the pinned tags into deps/, the # omx-contract header the LV2 measurer reads its rates from, and omx-clap-core's # LV2 feature provider (libomx-clap-lv2), built and installed into deps/ -# build/parity/lv2_measure openmixer's LV2 measurer (parity/openmixer, kept as it was there), built with -# these flags when deps/ holds that header: the parity test runs it beside ours -# make -C native install DESTDIR=... LIBEXECDIR=... the prober, the measurer and the interposer, for a package +# make -C native install DESTDIR=... LIBEXECDIR=... the prober, the measurer, the interposer and the LV2 host +# probe, for a package # # The CLAP headers come from `pkg-config clap` (Fedora clap-devel), else deps/clap; plugin-hostd's pin # header from `pkg-config plugin-hostd` (plugin-hostd-devel), else deps/plugin-hostd. omx-contract's limits @@ -30,8 +29,8 @@ BUILD ?= build CLAP_REF := 1.2.6 PLUGIN_HOSTD_REF := v0.1.4 -# the omx-contract `make deps` fetches where no omx-contract-devel is installed: the packages' floor, so both builds -# read the same contract. The rates are the same in 2.3.0, which openmixer's copy (parity/openmixer/BLOBS) was built against. +# the omx-contract `make deps` fetches where no omx-contract-devel is installed: the packages' floor, so the build +# reads the contract the packages do. OMX_CONTRACT_VERSION := 2.10.1 # the omx-clap-core whose LV2 feature provider `make deps` builds where no omx-clap-lv2-devel is installed: the # packages' floor @@ -82,8 +81,6 @@ all: $(BUILD)/clap-qualify $(BUILD)/lv2_measure $(BUILD)/lv2_host_probe $(BUILD) $(BUILD)/fixtures/fail-alloc.so \ $(BUILD)/fixtures/lv2/rt-safe.lv2/gain.so $(BUILD)/fixtures/lv2/rt-unsafe.lv2/gain.so \ $(BUILD)/fixtures/lv2/host-faithfulness.lv2/host-faithfulness.so - @# asked when the recipe runs, so `make deps all` builds it from what deps just fetched - @if [ -f deps/omx-contract/include/omxcontract/omx_contract_limits.h ]; then $(MAKE) --no-print-directory $(BUILD)/parity/lv2_measure; fi $(BUILD) $(BUILD)/fixtures: mkdir -p $@ @@ -94,8 +91,6 @@ $(BUILD)/clap-qualify: clap-qualify.c rt-interpose.h | $(BUILD) LV2_MEASURE_SRC := lv2-measure.c lv2-measure-options.c lv2-measure-pin.c LV2_MEASURE_HDR := lv2-measure-options.h lv2-measure-pin.h rt-interpose.h -# openmixer's copy, as it was taken: its own features unit -PARITY_SRC := lv2-measure.c lv2-measure-features.c lv2-measure-pin.c # as openmixer built it: no FMA contraction, so a figure is the same on every architecture and compiler $(BUILD)/lv2_measure: $(LV2_MEASURE_SRC) $(LV2_MEASURE_HDR) | $(BUILD) @@ -115,12 +110,6 @@ $(BUILD)/lv2_measure_options_test: lv2_measure_options.test.c lv2-measure-option $(CC) $(CPPFLAGS) $(CFLAGS) -Werror $(OMX_CLAP_LV2_CFLAGS) $(LV2_CFLAGS) -I. $(LDFLAGS) -o $@ lv2_measure_options.test.c \ lv2-measure-options.c $(OMX_CLAP_LV2_LIBS) -ldl -lpthread -# named lv2_measure too: the measurer leaves frames of its own binary, by that name, out of an event's owner -$(BUILD)/parity/lv2_measure: $(addprefix parity/openmixer/,$(PARITY_SRC)) parity/omx_console_limits.h | $(BUILD) - mkdir -p $(@D) - $(CC) $(CPPFLAGS) $(CFLAGS) -ffp-contract=off -Iparity -Ideps/omx-contract/include $(PLUGIN_HOSTD_CFLAGS) \ - $(LILV_CFLAGS) $(LDFLAGS) -o $@ $(addprefix parity/openmixer/,$(PARITY_SRC)) $(LILV_LIBS) -lpthread -lm -ldl - $(BUILD)/lv2_host_probe: lv2-host-probe.c | $(BUILD) $(CC) $(CPPFLAGS) $(CFLAGS) $(LILV_CFLAGS) $(LDFLAGS) -o $@ $< $(LILV_LIBS) -lpthread -lm @@ -179,11 +168,12 @@ deps: # the fixtures are the tests' own, never installed; the interposer is executable so rpm strips it (dh_fixperms clears the bit) # exactly src/qualify-declarations.ts's NATIVE_TOOLS (src/native-install-dirs.test.ts holds the two together) -install: $(BUILD)/clap-qualify $(BUILD)/lv2_measure $(BUILD)/rt_interpose.so +install: $(BUILD)/clap-qualify $(BUILD)/lv2_measure $(BUILD)/rt_interpose.so $(BUILD)/lv2_host_probe install -d $(DESTDIR)$(LIBEXECDIR) install -m 0755 $(BUILD)/clap-qualify $(DESTDIR)$(LIBEXECDIR)/clap-qualify install -m 0755 $(BUILD)/lv2_measure $(DESTDIR)$(LIBEXECDIR)/lv2_measure install -m 0755 $(BUILD)/rt_interpose.so $(DESTDIR)$(LIBEXECDIR)/rt_interpose.so + install -m 0755 $(BUILD)/lv2_host_probe $(DESTDIR)$(LIBEXECDIR)/lv2_host_probe clean: rm -rf $(BUILD) diff --git a/native/fetch-deps.sh b/native/fetch-deps.sh index 7672e68..c00d5e2 100755 --- a/native/fetch-deps.sh +++ b/native/fetch-deps.sh @@ -3,8 +3,8 @@ # Copyright (C) 2026 Pau Aliagas # # The headers the prober builds against, each at a release tag: the CLAP SDK and plugin-hostd (its pin.h). -# The omx-contract C header both LV2 measurers (ours and openmixer's copy, parity/openmixer) read their -# rates from, at the version the packages require, for a build with no omx-contract-devel installed. +# The omx-contract C header the LV2 measurer reads its rates from, at the version the packages require, +# for a build with no omx-contract-devel installed. # omx-clap-core's LV2 feature provider, the static libomx-clap-lv2 and its header, at the version the # packages require, built against the fetched CLAP headers and installed into /omx-clap-lv2, for a # build with no omx-clap-lv2-devel installed. diff --git a/native/parity/omx_console_limits.h b/native/parity/omx_console_limits.h deleted file mode 100644 index a2686df..0000000 --- a/native/parity/omx_console_limits.h +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2026 Pau Aliagas -/* - * Stands in for openmixer's console limits header when its copy of the LV2 measurer is built for the - * parity test. Of it, the measurer reads only OMX_DECLARED_RATES and OMX_DECLARED_RATE_COUNT, which it - * takes from omx-contract's header: so this includes that header, at the version openmixer pinned, - * and nothing of openmixer's own. - */ -#include diff --git a/native/parity/openmixer/BLOBS b/native/parity/openmixer/BLOBS deleted file mode 100644 index 55053fc..0000000 --- a/native/parity/openmixer/BLOBS +++ /dev/null @@ -1,8 +0,0 @@ -# openmixer's LV2 measurer at 0c4febac5d9fed3d5f9897d60980232d74020213, packages/pipewire-native/tools/: -# each file's git blob id there. src/lv2-measure-parity.test.ts holds the files here to them. -8fb867132fc56e4e2b5e5f471cd5af29afce3e1c lv2-measure-features.c -f1d5a6ca99a1aaeb1a81d18479e35e55bbc9270c lv2-measure-features.h -e9fc290b6e22b0a9e64ba0933dfeaf1448d76e75 lv2-measure-pin.c -0a78ca9c175a77932f4f49c54bc9af1f9b91e946 lv2-measure-pin.h -74af949c74da0898b826112c58eca59f23b474ed lv2-measure.c -6c06e9a5564dd56619092ee89258d7c0c999853b rt-interpose.h diff --git a/native/parity/openmixer/lv2-measure-features.c b/native/parity/openmixer/lv2-measure-features.c deleted file mode 100644 index 8fb8671..0000000 --- a/native/parity/openmixer/lv2-measure-features.c +++ /dev/null @@ -1,180 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2026 Pau Aliagas -/* - * lv2-measure-features.c — the LV2 host features of the verdict's measurement (lv2-measure-features.h; - * tools/lv2-measure.c). Control and worker threads; a plugin that maps inside run() takes the lock, which - * the verdict's rtSafety interposer counts. - */ -#include "lv2-measure-features.h" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -const char *const OMX_LV2_FEATURE_URIS[OMX_LV2_FEATURE_KINDS] = { - [OMX_LV2_F_MAP] = LV2_URID__map, - [OMX_LV2_F_UNMAP] = LV2_URID__unmap, - [OMX_LV2_F_OPTIONS] = LV2_OPTIONS__options, - [OMX_LV2_F_BOUNDED] = LV2_BUF_SIZE__boundedBlockLength, - [OMX_LV2_F_WORKER] = LV2_WORKER__schedule, - [OMX_LV2_F_DEFAULT_STATE] = LV2_STATE__loadDefaultState, - [OMX_LV2_F_LOG] = LV2_LOG__log, -}; - -int omx_lv2_feature_kind(const char *uri) { - for (int k = 0; uri && k < OMX_LV2_FEATURE_KINDS; k++) - if (strcmp(OMX_LV2_FEATURE_URIS[k], uri) == 0) return k; - return -1; -} - -int omx_lv2_feature_mask(const char *const *uris, uint32_t *mask) { - uint32_t m = 0; - for (uint32_t i = 0; uris && uris[i]; i++) { - const int k = omx_lv2_feature_kind(uris[i]); - if (k < 0) return -1; - m |= OMX_LV2_FEATURE_BIT(k); - } - *mask = m; - return 0; -} - -/* ---- the URI table: a URID is the index + 1 (0 is "no URID") ---- */ - -static struct { - pthread_mutex_t lock; - char **uris; - uint32_t n, cap; -} g_urid = {PTHREAD_MUTEX_INITIALIZER, NULL, 0, 0}; - -LV2_URID omx_lv2_urid_map(const char *uri) { - if (!uri) return 0; - LV2_URID id = 0; - pthread_mutex_lock(&g_urid.lock); - for (uint32_t i = 0; i < g_urid.n && !id; i++) - if (strcmp(g_urid.uris[i], uri) == 0) id = i + 1; - if (!id) { - if (g_urid.n == g_urid.cap && g_urid.cap <= UINT32_MAX / 2u) { - const uint32_t cap = g_urid.cap ? 2u * g_urid.cap : 64u; - char **grown = realloc(g_urid.uris, (size_t)cap * sizeof *grown); - if (grown) g_urid.uris = grown, g_urid.cap = cap; - } - char *copy = g_urid.n < g_urid.cap ? strdup(uri) : NULL; - if (copy) { - g_urid.uris[g_urid.n++] = copy; - id = g_urid.n; - } - } - pthread_mutex_unlock(&g_urid.lock); - return id; -} - -const char *omx_lv2_urid_unmap(LV2_URID id) { - pthread_mutex_lock(&g_urid.lock); - const char *uri = id >= 1 && id <= g_urid.n ? g_urid.uris[id - 1] : NULL; - pthread_mutex_unlock(&g_urid.lock); - return uri; -} - -void omx_lv2_urid_forget(void) { - pthread_mutex_lock(&g_urid.lock); - for (uint32_t i = 0; i < g_urid.n; i++) free(g_urid.uris[i]); - free(g_urid.uris); - g_urid.uris = NULL; - g_urid.n = g_urid.cap = 0; - pthread_mutex_unlock(&g_urid.lock); -} - -static LV2_URID feature_map(LV2_URID_Map_Handle h, const char *uri) { - (void)h; - return omx_lv2_urid_map(uri); -} -static const char *feature_unmap(LV2_URID_Unmap_Handle h, LV2_URID id) { - (void)h; - return omx_lv2_urid_unmap(id); -} - -/* log:log — formats nothing, writes nothing: a line from run() must never reach a syscall. */ -static int log_vprintf(LV2_Log_Handle h, LV2_URID type, const char *fmt, va_list ap) { - (void)h, (void)type, (void)fmt, (void)ap; - return 0; -} -static int log_printf(LV2_Log_Handle h, LV2_URID type, const char *fmt, ...) { - (void)h, (void)type, (void)fmt; - return 0; -} - -static LV2_URID_Map g_map = {NULL, feature_map}; -static LV2_URID_Unmap g_unmap = {NULL, feature_unmap}; -static LV2_Log_Log g_log = {NULL, log_printf, log_vprintf}; - -void omx_lv2_options_build(struct omx_lv2_options *o, double rate, uint32_t max_block) { - const LV2_URID t_int = omx_lv2_urid_map(LV2_ATOM__Int), t_float = omx_lv2_urid_map(LV2_ATOM__Float); - o->rate = (float)rate; - o->min_block = 1; - o->max_block = (int32_t)max_block; - const LV2_Options_Option opts[5] = { - {LV2_OPTIONS_INSTANCE, 0, omx_lv2_urid_map(LV2_PARAMETERS__sampleRate), sizeof(float), t_float, &o->rate}, - {LV2_OPTIONS_INSTANCE, 0, omx_lv2_urid_map(LV2_BUF_SIZE__minBlockLength), sizeof(int32_t), t_int, &o->min_block}, - {LV2_OPTIONS_INSTANCE, 0, omx_lv2_urid_map(LV2_BUF_SIZE__maxBlockLength), sizeof(int32_t), t_int, &o->max_block}, - {LV2_OPTIONS_INSTANCE, 0, omx_lv2_urid_map(LV2_BUF_SIZE__nominalBlockLength), sizeof(int32_t), t_int, &o->max_block}, - {LV2_OPTIONS_INSTANCE, 0, 0, 0, 0, NULL}, - }; - memcpy(o->opts, opts, sizeof opts); -} - -const LV2_Feature *const *omx_lv2_features_build(struct omx_lv2_feature_block *b, uint32_t mask, - struct omx_lv2_options *options, - LV2_Worker_Schedule *schedule) { - if (mask >> OMX_LV2_FEATURE_KINDS) return NULL; - uint32_t n = 0; - for (int k = 0; k < OMX_LV2_FEATURE_KINDS; k++) { - if (!(mask & OMX_LV2_FEATURE_BIT(k))) continue; - void *data = NULL; - switch ((enum omx_lv2_feature_kind)k) { - case OMX_LV2_F_MAP: data = &g_map; break; - case OMX_LV2_F_UNMAP: data = &g_unmap; break; - case OMX_LV2_F_LOG: data = &g_log; break; - case OMX_LV2_F_OPTIONS: - if (!options) return NULL; - data = options->opts; - break; - case OMX_LV2_F_WORKER: - if (!schedule) return NULL; - data = schedule; - break; - case OMX_LV2_F_BOUNDED: - case OMX_LV2_F_DEFAULT_STATE: - case OMX_LV2_FEATURE_KINDS: break; /* flags: the promise is the presence */ - } - b->features[n] = (LV2_Feature){OMX_LV2_FEATURE_URIS[k], data}; - b->ptrs[n] = &b->features[n]; - n++; - } - b->ptrs[n] = NULL; - return b->ptrs; -} - -static const LV2_Descriptor *lv2_refuse(const char **why, const char *step) { - if (why) *why = step; - return NULL; -} - -const LV2_Descriptor *omx_lv2_open(void **so, const char *path, const char *uri, const char **why) { - *so = dlopen(path, RTLD_NOW | RTLD_LOCAL); - if (!*so) return lv2_refuse(why, "dlopen"); - LV2_Descriptor_Function fn; - *(void **)&fn = dlsym(*so, "lv2_descriptor"); - const LV2_Descriptor *d = NULL; - for (uint32_t i = 0; fn && (d = fn(i)) != NULL; i++) - if (strcmp(d->URI, uri) == 0) return d; - dlclose(*so); - *so = NULL; - return lv2_refuse(why, fn ? "uri" : "lv2_descriptor"); -} diff --git a/native/parity/openmixer/lv2-measure-features.h b/native/parity/openmixer/lv2-measure-features.h deleted file mode 100644 index f1d5a6c..0000000 --- a/native/parity/openmixer/lv2-measure-features.h +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2026 Pau Aliagas -/* - * lv2-measure-features.h — THE LV2 host features of the verdict's measurement (tools/lv2-measure.c; - * docs/design/specs/2026-09-29-host-backend-one-contract.md §3, "Where the providers live"). The - * engine hosts LV2 only through libomx-clap-lv2, whose providers serve the same kinds of the - * verdict's OMX_INPROCESS_FEATURES; this unit is the measurement's own host, built on the same - * kinds. - * - * WHICH features a host serves is the host's list; this unit only says how each is BUILT, and - * builds an array of exactly the kinds a mask names. - * - * The URI table is ONE per process (a URID is a process fact atoms carry between instances), - * mutex-guarded for the control and worker threads. ONE capacity policy: it grows until an - * allocation fails, then `map` answers 0 (the LV2 "no URID"). It COPIES every string it keeps — - * a caller's pointer is never stored. Strings live until omx_lv2_urid_forget. - * - * It is also the ONE door that opens an LV2 binary by path (omx_lv2_open): a CLAP binary opens - * through libomx-clap-core (omx_clap_host_binary_open, omx_clap_host_open), never here. - */ -#ifndef LV2_MEASURE_FEATURES_H -#define LV2_MEASURE_FEATURES_H - -#include - -#include -#include -#include -#include -#include - -/* The provider order: an array is built in this order, whatever order a list names them in. */ -enum omx_lv2_feature_kind { - OMX_LV2_F_MAP, - OMX_LV2_F_UNMAP, - OMX_LV2_F_OPTIONS, - OMX_LV2_F_BOUNDED, - OMX_LV2_F_WORKER, - OMX_LV2_F_DEFAULT_STATE, - OMX_LV2_F_LOG, - OMX_LV2_FEATURE_KINDS -}; -#define OMX_LV2_FEATURE_BIT(k) (1u << (k)) - -/* What a lilv-based tool host serves (lv2-measure.c): every kind but - * loadDefaultState, which in-process is the registration's hook and under lilv is lilv-state's. */ -#define OMX_LV2_LILV_HOST_FEATURES \ - (OMX_LV2_FEATURE_BIT(OMX_LV2_F_MAP) | OMX_LV2_FEATURE_BIT(OMX_LV2_F_UNMAP) | \ - OMX_LV2_FEATURE_BIT(OMX_LV2_F_OPTIONS) | OMX_LV2_FEATURE_BIT(OMX_LV2_F_BOUNDED) | \ - OMX_LV2_FEATURE_BIT(OMX_LV2_F_WORKER) | OMX_LV2_FEATURE_BIT(OMX_LV2_F_LOG)) - -/* The URI of each kind's feature. */ -extern const char *const OMX_LV2_FEATURE_URIS[OMX_LV2_FEATURE_KINDS]; - -/* The kind serving `uri`, or -1 when this unit has no provider for it. */ -int omx_lv2_feature_kind(const char *uri); - -/* The mask of a NULL-terminated URI list (NULL: the empty list). -1 when the list names a feature - * with no provider — never deliver less than the list. */ -int omx_lv2_feature_mask(const char *const *uris, uint32_t *mask); - -/* ---- urid:map / urid:unmap: the one URI table ---- */ -LV2_URID omx_lv2_urid_map(const char *uri); /* 0 for NULL or when the table cannot grow */ -const char *omx_lv2_urid_unmap(LV2_URID id); /* NULL for 0 or an id never handed out */ -void omx_lv2_urid_forget(void); /* frees every kept string; ids restart at 1 */ - -/* ---- options:options: param:sampleRate (Float), bufsz:minBlockLength 1, bufsz:maxBlockLength and - * bufsz:nominalBlockLength (Int, the block). The options point INTO the struct: build it where it - * stays. ---- */ -struct omx_lv2_options { - float rate; - int32_t min_block, max_block; - LV2_Options_Option opts[5]; -}; -void omx_lv2_options_build(struct omx_lv2_options *o, double rate, uint32_t max_block); - -/* ---- the feature array ---- */ -struct omx_lv2_feature_block { - LV2_Feature features[OMX_LV2_FEATURE_KINDS]; - const LV2_Feature *ptrs[OMX_LV2_FEATURE_KINDS + 1]; -}; - -/* Fill `b` with exactly the kinds `mask` names, in provider order, NULL-terminated, and return the - * array `instantiate` takes. `options` serves OMX_LV2_F_OPTIONS, `schedule` OMX_LV2_F_WORKER (the - * instance's own); boundedBlockLength and loadDefaultState are flags, their presence the promise. - * NULL — nothing built — when the mask names a kind whose data the caller did not hand over, or a - * bit outside the kinds: refused, never a shorter array. */ -const LV2_Feature *const *omx_lv2_features_build(struct omx_lv2_feature_block *b, uint32_t mask, - struct omx_lv2_options *options, - LV2_Worker_Schedule *schedule); - -/* ---- the door: open the LV2 `.so` at `path` and walk `lv2_descriptor` to `uri`. Control thread. - * The descriptor, `*so` holding the handle to dlclose after the last instance; NULL refused (`*so` - * NULL, nothing loaded) with `*why` the step: "dlopen", "lv2_descriptor" or "uri" (a static - * string). ---- */ -const LV2_Descriptor *omx_lv2_open(void **so, const char *path, const char *uri, const char **why); - -#endif diff --git a/native/parity/openmixer/lv2-measure-pin.c b/native/parity/openmixer/lv2-measure-pin.c deleted file mode 100644 index e9fc290..0000000 --- a/native/parity/openmixer/lv2-measure-pin.c +++ /dev/null @@ -1,230 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2026 Pau Aliagas -/* - * lv2-measure-pin.c — the integrity block of an LV2 plugin; see lv2-measure-pin.h. Nothing here - * hashes or serialises: plugin-hostd's pin.h does both, over the records lilv fills. - */ -#define _GNU_SOURCE -#include "lv2-measure-pin.h" - -#include -#include -#include -#include - -#include -#include - -#define MANIFEST "manifest.ttl" -#define PIN_FILES_MAX 64 - -struct pin_file { - char *path; - char sha256[PHD_SHA256_HEX_LEN + 1]; -}; - -static void json_text(FILE *out, const char *s) { - fputc('"', out); - for (const char *p = s; *p; p++) { - if (*p == '"' || *p == '\\') fprintf(out, "\\%c", *p); - else if ((unsigned char)*p < 0x20) fprintf(out, "\\u%04x", *p); - else fputc(*p, out); - } - fputc('"', out); -} - -static void print_reason(FILE *out, const char *reason, const char *detail) { - fprintf(out, "\"unmeasuredReason\":"); - json_text(out, reason); - fprintf(out, ",\"unmeasuredDetail\":"); - json_text(out, detail); -} - -/* the plugin's version as the interchange writes it: lv2:minorVersion.lv2:microVersion */ -static char *plugin_version(LilvWorld *world, const LilvPlugin *plugin) { - LilvNode *minor_p = lilv_new_uri(world, LV2_CORE__minorVersion); - LilvNode *micro_p = lilv_new_uri(world, LV2_CORE__microVersion); - /* through the plugin, so its data files are loaded first */ - LilvNodes *minor = lilv_plugin_get_value(plugin, minor_p); - LilvNodes *micro = lilv_plugin_get_value(plugin, micro_p); - const LilvNode *mi = minor ? lilv_nodes_get_first(minor) : NULL; - const LilvNode *mc = micro ? lilv_nodes_get_first(micro) : NULL; - char *version = NULL; - if (mi && mc && lilv_node_is_int(mi) && lilv_node_is_int(mc) && - asprintf(&version, "%d.%d", lilv_node_as_int(mi), lilv_node_as_int(mc)) < 0) - version = NULL; - lilv_nodes_free(minor); - lilv_nodes_free(micro); - lilv_node_free(minor_p); - lilv_node_free(micro_p); - return version; -} - -/* a file of the bundle by its IRI: its path relative to the bundle root, or NULL with why */ -static char *bundle_relative(const char *bundle, const LilvNode *iri, const char **why) { - const char *text = lilv_node_as_uri(iri); - if (strchr(text, '%')) { - *why = "a file the plugin loads is named by a percent-encoded IRI, which plugin-hostd does not decode"; - return NULL; - } - char *path = lilv_file_uri_parse(text, NULL); - if (!path) { - *why = "a file the plugin loads is not a file: IRI"; - return NULL; - } - const size_t n = strlen(bundle); - char *rel = NULL; - if (strncmp(path, bundle, n) == 0 && path[n] != '\0') rel = strdup(path + n); - lilv_free(path); - if (!rel) *why = "a file the plugin loads lies outside its bundle"; - else if (strpbrk(rel, PHD_PIN_PATH_EXCLUDED)) { - *why = "a file name holds a separator of pin_set or whitespace"; - free(rel); - rel = NULL; - } - return rel; -} - -static int add_file(struct pin_file *files, int *n, char *rel) { - for (int i = 0; i < *n; i++) - if (!strcmp(files[i].path, rel)) { - free(rel); - return 0; - } - if (*n == PIN_FILES_MAX) { - free(rel); - return -1; - } - files[(*n)++].path = rel; - return 0; -} - -/* whether the manifest sets a base, which plugin-hostd does not resolve */ -static int manifest_based(const char *bundle) { - char path[4096]; - snprintf(path, sizeof path, "%s%s", bundle, MANIFEST); - FILE *f = fopen(path, "r"); - if (!f) return 0; - char line[4096]; - int based = 0; - while (!based && fgets(line, sizeof line, f)) { - const char *p = line + strspn(line, " \t"); - based = !strncmp(p, "@base", 5) || !strncasecmp(p, "BASE", 4); - } - fclose(f); - return based; -} - -static int by_path(const void *a, const void *b) { - return strcmp(((const struct pin_file *)a)->path, ((const struct pin_file *)b)->path); -} - -/* the omx-layout/1 fingerprint of the plugin's control ports; 0, or -1 */ -static int layout_digest(LilvWorld *world, const LilvPlugin *plugin, char hex[PHD_SHA256_HEX_LEN + 1]) { - const uint32_t n_ports = lilv_plugin_get_num_ports(plugin); - LilvNode *control = lilv_new_uri(world, LV2_CORE__ControlPort); - LilvNode *output = lilv_new_uri(world, LV2_CORE__OutputPort); - phd_layout_lv2_port_t *ports = calloc(n_ports ? n_ports : 1, sizeof *ports); - LilvNodes **props = calloc(n_ports ? n_ports : 1, sizeof *props); - const char ***names = calloc(n_ports ? n_ports : 1, sizeof *names); - size_t count = 0; - int ret = -1; - for (uint32_t i = 0; ports && props && names && i < n_ports; i++) { - const LilvPort *port = lilv_plugin_get_port_by_index(plugin, i); - if (!lilv_port_is_a(plugin, port, control)) continue; - phd_layout_lv2_port_t *p = &ports[count]; - LilvNode *def = NULL, *min = NULL, *max = NULL; - lilv_port_get_range(plugin, port, &def, &min, &max); - p->index = lilv_port_get_index(plugin, port); - p->symbol = lilv_node_as_string(lilv_port_get_symbol(plugin, port)); - p->output = lilv_port_is_a(plugin, port, output); - if (min) p->has |= PHD_LAYOUT_HAS_MIN, p->min = lilv_node_as_float(min); - if (max) p->has |= PHD_LAYOUT_HAS_MAX, p->max = lilv_node_as_float(max); - if (def) p->has |= PHD_LAYOUT_HAS_DEF, p->def = lilv_node_as_float(def); - lilv_node_free(def); - lilv_node_free(min); - lilv_node_free(max); - props[count] = lilv_port_get_properties(plugin, port); - const unsigned np = lilv_nodes_size(props[count]); - names[count] = calloc(np ? np : 1, sizeof(char *)); - unsigned k = 0; - LILV_FOREACH (nodes, it, props[count]) names[count][k++] = lilv_node_as_uri(lilv_nodes_get(props[count], it)); - p->properties = names[count]; - p->nproperties = k; - count++; - } - if (ports && props && names) ret = phd_layout_lv2_digest(ports, count, hex); - for (size_t i = 0; props && names && i < count; i++) { - lilv_nodes_free(props[i]); - free(names[i]); - } - free(names); - free(props); - free(ports); - lilv_node_free(control); - lilv_node_free(output); - return ret; -} - -void omx_lv2_pin_print(FILE *out, LilvWorld *world, const LilvPlugin *plugin) { - char *version = plugin_version(world, plugin); - if (version) { - fprintf(out, ",\"version\":"); - json_text(out, version); - } - fprintf(out, ",\"integrity\":{"); - if (version) { - fprintf(out, "\"version\":"); - json_text(out, version); - fputc(',', out); - } - free(version); - - struct pin_file files[PIN_FILES_MAX]; - int n = 0; - const char *why = NULL; - char *bundle = lilv_file_uri_parse(lilv_node_as_uri(lilv_plugin_get_bundle_uri(plugin)), NULL); - if (!bundle || bundle[0] == '\0' || bundle[strlen(bundle) - 1] != '/') why = "the bundle is not a directory"; - else if (manifest_based(bundle)) why = "the manifest sets @base, which plugin-hostd does not resolve"; - else { - add_file(files, &n, strdup(MANIFEST)); - const LilvNode *library = lilv_plugin_get_library_uri(plugin); - char *rel = library ? bundle_relative(bundle, library, &why) : NULL; - if (!library) why = "the plugin names no lv2:binary"; - if (rel && add_file(files, &n, rel) != 0) why = "more files than a pin holds"; - const LilvNodes *data = lilv_plugin_get_data_uris(plugin); - LILV_FOREACH (nodes, it, data) { - if (why) break; - const char *w = NULL; - char *file = bundle_relative(bundle, lilv_nodes_get(data, it), &w); - /* a file of another bundle is that bundle's: a worker's world is this one bundle */ - if (!file && w && !strstr(w, "outside")) why = w; - if (file && add_file(files, &n, file) != 0) why = "more files than a pin holds"; - } - } - char layout[PHD_SHA256_HEX_LEN + 1]; - for (int i = 0; !why && i < n; i++) { - char path[4096]; - snprintf(path, sizeof path, "%s%s", bundle, files[i].path); - const int fd = open(path, O_RDONLY | O_CLOEXEC); - if (fd < 0 || phd_pin_fd_digest(fd, files[i].sha256) != 0) why = "a file the plugin loads cannot be read"; - if (fd >= 0) close(fd); - } - if (!why && layout_digest(world, plugin, layout) != 0) why = "the layout could not be read"; - - if (why) { - print_reason(out, strstr(why, "cannot be read") || strstr(why, "could not") ? "probe-error" : "unpinnable", why); - } else { - qsort(files, (size_t)n, sizeof files[0], by_path); - fprintf(out, "\"files\":["); - for (int i = 0; i < n; i++) { - fprintf(out, "%s{\"path\":", i ? "," : ""); - json_text(out, files[i].path); - fprintf(out, ",\"sha256\":\"%s\"}", files[i].sha256); - } - fprintf(out, "],\"layout\":{\"scheme\":\"%s\",\"sha256\":\"%s\"}", PHD_PIN_LAYOUT_SCHEME, layout); - } - fputc('}', out); - for (int i = 0; i < n; i++) free(files[i].path); - lilv_free(bundle); -} diff --git a/native/parity/openmixer/lv2-measure-pin.h b/native/parity/openmixer/lv2-measure-pin.h deleted file mode 100644 index 0a78ca9..0000000 --- a/native/parity/openmixer/lv2-measure-pin.h +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2026 Pau Aliagas -/* - * lv2-measure-pin.h — the pin `lv2-measure` records for an LV2 plugin - * (docs/design/specs/2026-09-25-plugin-qualify.md §11; lv2-measurement-interchange.md §3c). - * - * The files: the bundle's `manifest.ttl`, the shared object `lv2:binary` names and every file - * `rdfs:seeAlso` names for the plugin, each by its path relative to the bundle root, hashed over one - * open descriptor. The layout: every control port, input and output, as the loaded world describes - * it, in plugin-hostd's omx-layout/1. Both are plugin-hostd's `pin.h` (one-contract §10.2): this - * unit only fills its records from lilv. A plugin whose files cannot be pinned (a path holding a - * separator of `pin_set` or whitespace, a manifest using `@base`, a percent-encoded IRI) prints a - * reason and no pin, never a partial one. - */ -#ifndef LV2_MEASURE_PIN_H -#define LV2_MEASURE_PIN_H - -#include - -#include - -/** Print `,"version":"."` when the plugin declares one, then `,"integrity":{...}`. */ -void omx_lv2_pin_print(FILE *out, LilvWorld *world, const LilvPlugin *plugin); - -#endif diff --git a/native/parity/openmixer/lv2-measure.c b/native/parity/openmixer/lv2-measure.c deleted file mode 100644 index 74af949..0000000 --- a/native/parity/openmixer/lv2-measure.c +++ /dev/null @@ -1,723 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2026 Pau Aliagas -/* - * lv2-measure — the facts `classifyPluginHosting` refuses to work without. - * - * The classifier fails closed: with no measurement every plugin reads `unknown` and - * stays on the isolated host. That is the correct default and a useless one — it is - * this program's whole job to replace it with numbers. It reports, per plugin: - * - * features required/optional LV2 features and CV ports, read from the bundle - * rtSafety allocations, locks and syscalls counted INSIDE run(), under a sweep - * stability instantiate/destroy lifecycles, and threads the plugin leaves behind - * - * WHY A REAL HOST. `lv2-host-probe.c` beside this one exists because a plugin's - * host-facing half — the manifest lilv parses, the ports it binds, instantiate's - * allocation — can be wrong with the DSP perfectly green. The same argument applies - * to measurement: reading a plugin's source for mallocs would miss what its - * dependencies do, and asking the plugin is not measuring it. So this loads the - * plugin the way mod-host does, through lilv, and watches what it actually does. - * - * THE SWEEP IS PART OF THE METHOD, NOT A REFINEMENT. A compressor that allocates - * when its ratio crosses a threshold is RT-safe at rest and dangerous in use. Every - * control port is swept across its declared range during the measured window, so the - * figure describes the plugin as an operator will drive it. rtSafety measured - * without the sweep is reported as unswept and the classifier treats it as - * unmeasured. - * - * WHAT IT DOES NOT DO. It does not soak — the spec's stability dimension also wants a - * continuous run at least as long as the operator's longest show, which is hours and - * belongs in a scheduled sweep rather than here. A plugin passing this program still - * reads `unknown` until it has been soaked, and that is deliberate. - * - * npm run build:lv2-measure (cc ... -Isrc tools/lv2-measure.c tools/lv2-measure-features.c - * tools/lv2-measure-pin.c, plugin-hostd's include directory, - * lilv, -lpthread -lm -ldl: package.json carries the line) - * LD_PRELOAD=$PWD/build/rt_interpose.so ./build/lv2_measure [rate] [block] [cycles] - * ./build/lv2_measure --pin the pin alone, nothing instantiated - * - * The measurement line also carries the plugin's pin, `integrity` (lv2-measure-pin.h): the - * SHA-256 of every file the host loads and the parameter-layout fingerprint, through - * plugin-hostd's pin.h. - * - * Emits one JSON object on stdout. Exit 0 measured, 2 could not instantiate, 3 no - * such plugin — a host that cannot find a plugin and a host that found a broken one - * are different failures and must not share an exit code. - */ -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include "rt-interpose.h" -#include "lv2-measure-features.h" -#include "lv2-measure-pin.h" -#include "omx_console_limits.h" -#include - -/* - * THE HOST FEATURES WE PROVIDE ARE THE MEASUREMENT'S DEFINITION. - * - * A plugin measured under a host that offers more than ours does would earn a - * verdict our RT thread cannot honour; measured under one that offers less, a - * perfectly good plugin fails to instantiate and reads as broken. So the providers - * are lv2-measure-features.h's, built on the kinds the LV2 adapter the engine hosts - * through provides (host-backend-one-contract §3) — one URI table, the options at the - * measured rate and block, a silent log. The worker's schedule is this program's own - * (below). Instantiating with NULL - * features, which is what this program did first, made every LSP plugin segfault: - * they require urid:map, and a host that does not map URIs is not a host. - */ -/* - * The worker. A real host runs work() off the audio thread; scheduling from run() - * must not block. Jobs are queued here and drained by the main thread between - * blocks, which is a genuine non-RT execution rather than an inline call — a plugin - * that deadlocks or misbehaves when work() does not run inline is exactly what this - * catches, and calling work() inline would hide it. - * - * FINDING (2026-10-01, lane lv2-host-features-one-home, not fixed there): nothing in this - * program drains job_queue and `worker_respond` is never handed to a work() call — the - * build warns it is unused, which is why build:lv2-measure carries no -Werror. A plugin's - * scheduled work is queued and dropped, so the paragraph above describes the intent, not - * the measurement; the worker's non-RT execution is unmeasured until a drain lands. - */ -#define MAX_JOBS 64 -static struct { - uint32_t size; - uint8_t data[8192]; -} job_queue[MAX_JOBS]; -static int job_head, job_tail; - -static LV2_Worker_Status schedule_work(LV2_Worker_Schedule_Handle h, uint32_t size, - const void *data) { - (void)h; - int next = (job_tail + 1) % MAX_JOBS; - if (next == job_head || size > sizeof job_queue[0].data) return LV2_WORKER_ERR_NO_SPACE; - job_queue[job_tail].size = size; - memcpy(job_queue[job_tail].data, data, size); - job_tail = next; - return LV2_WORKER_SUCCESS; -} - -static LV2_Worker_Status worker_respond(LV2_Worker_Respond_Handle h, uint32_t size, - const void *data) { - (void)h; - (void)size; - (void)data; - return LV2_WORKER_SUCCESS; -} - -/* LV2_CORE__CVPort comes from lv2/core/lv2.h via lilv.h. */ - -/* The interposer's control surface, resolved at start-up. All NULL when the library - * was not preloaded, which is reported as no-interposer rather than as zero counts. */ -static void (*rt_begin)(void); -static void (*rt_end)(void); -static void (*rt_reset)(void); -static void (*rt_counts)(uint64_t *, uint64_t *, uint64_t *); -static int (*rt_present)(void); -static int (*rt_events)(struct omx_rt_event *, int); - -static void resolve_interposer(void) { - rt_begin = dlsym(RTLD_DEFAULT, "omx_rt_begin"); - rt_end = dlsym(RTLD_DEFAULT, "omx_rt_end"); - rt_reset = dlsym(RTLD_DEFAULT, "omx_rt_reset"); - rt_counts = dlsym(RTLD_DEFAULT, "omx_rt_counts"); - rt_present = dlsym(RTLD_DEFAULT, "omx_rt_present"); - rt_events = dlsym(RTLD_DEFAULT, "omx_rt_events"); -} - -/* Threads in this process, from /proc/self/task. The spec's `threadsAfterInstantiate` - * is a measured fact, not a guess from the plugin's class. */ -static int count_threads(void) { - DIR *d = opendir("/proc/self/task"); - if (d == NULL) return -1; - int n = 0; - for (struct dirent *e = readdir(d); e != NULL; e = readdir(d)) { - if (e->d_name[0] != '.') n++; - } - closedir(d); - return n; -} - -/* Resident set, in kB. A plugin that leaks shows here long before it crashes. */ -static long rss_kb(void) { - FILE *f = fopen("/proc/self/statm", "r"); - if (f == NULL) return -1; - long total = 0, res = 0; - if (fscanf(f, "%ld %ld", &total, &res) != 2) res = -1; - fclose(f); - return res > 0 ? res * (sysconf(_SC_PAGESIZE) / 1024) : -1; -} - -static const char *rt_kind_name(int k) { - switch (k) { - case OMX_RT_MALLOC: return "malloc"; case OMX_RT_CALLOC: return "calloc"; - case OMX_RT_REALLOC: return "realloc"; case OMX_RT_FREE: return "free"; - case OMX_RT_MUTEX_LOCK: return "mutex_lock"; case OMX_RT_MUTEX_TRYLOCK: return "mutex_trylock"; - case OMX_RT_OPEN: return "open"; case OMX_RT_NANOSLEEP: return "nanosleep"; - case OMX_RT_WRITE: return "write"; case OMX_RT_READ: return "read"; - default: return "?"; - } -} - -/* The frame that OWNS an event: the first one not inside the interposer, libc, the - * unwinder or this host. That is the plugin (or a library it calls), which is the fact a - * ruling needs — "one lock" becomes "one lock from lsp-plugins.so". */ -static void rt_owner(const struct omx_rt_event *e, const char **module, const char **symbol) { - *module = "?"; *symbol = "?"; - for (int i = 0; i < e->depth; i++) { - Dl_info info; - if (!dladdr(e->frames[i], &info) || !info.dli_fname) continue; - const char *base = strrchr(info.dli_fname, '/'); - base = base ? base + 1 : info.dli_fname; - if (strstr(base, "rt_interpose") || strstr(base, "libc.so") || strstr(base, "libgcc") || - strstr(base, "libpthread") || strstr(base, "lv2_measure") || strstr(base, "ld-linux")) - continue; - *module = base; - *symbol = info.dli_sname ? info.dli_sname : "?"; - return; - } -} - -/* A JSON string, escaped enough for URIs and plugin names. */ -static void json_str(FILE *out, const char *s) { - fputc('"', out); - for (const char *p = s; p && *p; p++) { - if (*p == '"' || *p == '\\') fprintf(out, "\\%c", *p); - else if ((unsigned char)*p < 0x20) fprintf(out, "\\u%04x", *p); - else fputc(*p, out); - } - fputc('"', out); -} - -/* `--pin `: the plugin's pin alone, read from the world without instantiating it — a re-vet of - * the bytes, and the line the console's pin_set is built from. Exit 0, or 3 when there is no such plugin. */ -static int pin_only(const char *uri) { - LilvWorld *world = lilv_world_new(); - lilv_world_load_all(world); - LilvNode *uri_node = lilv_new_uri(world, uri); - const LilvPlugin *plugin = lilv_plugins_get_by_uri(lilv_world_get_all_plugins(world), uri_node); - printf("{\"uri\":"); - json_str(stdout, uri); - printf(",\"found\":%s", plugin ? "true" : "false"); - if (plugin) omx_lv2_pin_print(stdout, world, plugin); - printf("}\n"); - lilv_node_free(uri_node); - lilv_world_free(world); - return plugin ? 0 : 3; -} - -int main(int argc, char **argv) { - if (argc == 3 && !strcmp(argv[1], "--pin")) return pin_only(argv[2]); - if (argc < 2) { - fprintf(stderr, "usage: %s [rate] [block] [cycles] | %s --pin \n", argv[0], argv[0]); - return 1; - } - const char *uri = argv[1]; - const double rate = argc > 2 ? atof(argv[2]) : 192000.0; - const uint32_t block = argc > 3 ? (uint32_t)atoi(argv[3]) : 512; - const int cycles = argc > 4 ? atoi(argv[4]) : 20; - /* RT safety is measured REPEATS times and reported as the worst case. A single - * sample is not a measurement here: LSP's gate swings between 0 and 74 allocations - * across identical runs, so one clean run would have licensed it onto the RT - * thread. The max is what an operator actually risks. */ - const int repeats = argc > 5 ? atoi(argv[5]) : 3; - const int warmup_blocks = 64, measured_blocks = 512; - - /* A tone excites a narrow band; noise excites everything. AmpVTS's rate-dependent - * death showed under noise and hid under a 1 kHz sine, so the stimulus is a knob - * and the sweep runs the one that reproduces known failures. */ - const int use_noise = getenv("OMX_STIM") && strcmp(getenv("OMX_STIM"), "noise") == 0; - unsigned rng = 12345u; - - resolve_interposer(); - - LilvWorld *world = lilv_world_new(); - lilv_world_load_all(world); - const LilvPlugins *plugins = lilv_world_get_all_plugins(world); - LilvNode *uri_node = lilv_new_uri(world, uri); - const LilvPlugin *plugin = lilv_plugins_get_by_uri(plugins, uri_node); - if (plugin == NULL) { - fprintf(stderr, "lv2-measure: no such plugin: %s\n", uri); - printf("{\"uri\":"); - json_str(stdout, uri); - printf(",\"found\":false}\n"); - lilv_node_free(uri_node); - lilv_world_free(world); - return 3; - } - - /* ---- features, straight from the bundle ---- */ - LilvNodes *required = lilv_plugin_get_required_features(plugin); - LilvNodes *optional = lilv_plugin_get_optional_features(plugin); - - /* ---- ports: classify, and find the control ports the sweep will drive ---- */ - const uint32_t n_ports = lilv_plugin_get_num_ports(plugin); - LilvNode *audio_class = lilv_new_uri(world, LV2_CORE__AudioPort); - LilvNode *control_class = lilv_new_uri(world, LV2_CORE__ControlPort); - LilvNode *input_class = lilv_new_uri(world, LV2_CORE__InputPort); - LilvNode *output_class = lilv_new_uri(world, LV2_CORE__OutputPort); - LilvNode *cv_class = lilv_new_uri(world, LV2_CORE__CVPort); - - float *mins = calloc(n_ports, sizeof(float)); - float *maxes = calloc(n_ports, sizeof(float)); - float *defs = calloc(n_ports, sizeof(float)); - lilv_plugin_get_port_ranges_float(plugin, mins, maxes, defs); - - uint32_t n_audio_in = 0, n_audio_out = 0, n_cv = 0, n_control_in = 0; - for (uint32_t i = 0; i < n_ports; i++) { - const LilvPort *port = lilv_plugin_get_port_by_index(plugin, i); - if (lilv_port_is_a(plugin, port, cv_class)) n_cv++; - else if (lilv_port_is_a(plugin, port, audio_class)) { - if (lilv_port_is_a(plugin, port, input_class)) n_audio_in++; - else if (lilv_port_is_a(plugin, port, output_class)) n_audio_out++; - } else if (lilv_port_is_a(plugin, port, control_class) && - lilv_port_is_a(plugin, port, input_class)) { - n_control_in++; - } - } - - - /* The host's feature block (lv2-measure-features.h), built once and used for every - * instantiate below; the rate probe rebuilds the options in place for each rate. */ - LV2_Worker_Schedule schedule = {NULL, schedule_work}; - static struct omx_lv2_options options; - omx_lv2_options_build(&options, rate, block); - struct omx_lv2_feature_block feature_block; - const LV2_Feature *const *features = - omx_lv2_features_build(&feature_block, OMX_LV2_LILV_HOST_FEATURES, &options, &schedule); - - - /* ---- SOAK MODE ---------------------------------------------------------- - * The stability dimension's real question is not "does it start" but "does it - * still work in the third hour". AmpVTS dies two seconds into a 96 kHz render; - * a leak shows only as RSS climbing over many minutes. So this runs one instance - * continuously, sweeps every control slowly across its range, and watches three - * things per one-second window: the output stays finite, it does not fall away - * from the level it started at, and the process does not grow. - * - * It short-circuits the rest of the program: a soak is a long measurement of one - * thing, not an addendum to the quick ones. - */ - const char *soak_env = getenv("OMX_SOAK_SECONDS"); - if (soak_env != NULL && atoi(soak_env) > 0) { - const double soak_s = atof(soak_env); - LilvInstance *si = lilv_plugin_instantiate(plugin, rate, features); - if (si == NULL) { - printf("{\"uri\":"); - json_str(stdout, uri); - printf(",\"soak\":{\"instantiated\":false}}\n"); - return 2; - } - const size_t slots = (size_t)n_audio_in + n_audio_out + n_cv + 2; - float *ab = calloc(slots * block, sizeof(float)); - float *cb = calloc(n_ports, sizeof(float)); - void **atb = calloc(n_ports, sizeof(void *)); - uint32_t sai = 0, sao = 0; - for (uint32_t i = 0; i < n_ports; i++) { - const LilvPort *port = lilv_plugin_get_port_by_index(plugin, i); - const int is_in = lilv_port_is_a(plugin, port, input_class); - if (lilv_port_is_a(plugin, port, audio_class) || lilv_port_is_a(plugin, port, cv_class)) { - size_t slot = is_in ? sai++ : (n_audio_in + n_cv + sao++); - if (slot >= slots) slot = slots - 1; - lilv_instance_connect_port(si, i, ab + slot * block); - } else if (lilv_port_is_a(plugin, port, control_class)) { - cb[i] = isnan(defs[i]) ? 0.0f : defs[i]; - lilv_instance_connect_port(si, i, cb + i); - } else { - const size_t cap = 8192; - uint8_t *b = calloc(1, cap); - atb[i] = b; - LV2_Atom_Sequence *sq = (LV2_Atom_Sequence *)b; - sq->atom.size = is_in ? (uint32_t)sizeof(LV2_Atom_Sequence_Body) - : (uint32_t)(cap - sizeof(LV2_Atom)); - lilv_instance_connect_port(si, i, b); - } - } - lilv_instance_activate(si); - - const long total_blocks = (long)(soak_s * rate / block); - const long win_blocks = (long)(rate / block); /* ~1 s */ - long b = 0, windows = 0, nonfinite_windows = 0, silent_windows = 0; - double first_rms = -1.0, last_rms = -1.0, min_rms = 1e30, max_rms = 0.0; - long rss_start = rss_kb(), rss_max = rss_start; - double win_sum = 0.0; - long win_n = 0; - for (b = 0; b < total_blocks; b++) { - const float t = (float)(b % 4096) / 4096.0f; /* slow, repeating parameter sweep */ - for (uint32_t i = 0; i < n_ports; i++) { - const LilvPort *port = lilv_plugin_get_port_by_index(plugin, i); - if (!lilv_port_is_a(plugin, port, control_class)) continue; - if (!lilv_port_is_a(plugin, port, input_class)) continue; - if (isnan(mins[i]) || isnan(maxes[i]) || maxes[i] <= mins[i]) continue; - cb[i] = mins[i] + (maxes[i] - mins[i]) * t; - } - for (uint32_t i = 0; i < n_audio_in; i++) { - for (uint32_t f = 0; f < block; f++) { - rng = rng * 1103515245u + 12345u; - ab[i * block + f] = 0.1f * ((float)((rng >> 16) & 0x7fff) / 16384.0f - 1.0f); - } - } - lilv_instance_run(si, block); - if (n_audio_out > 0) { - const float *o = ab + (size_t)(n_audio_in + n_cv) * block; - int win_nonfinite = 0; - for (uint32_t f = 0; f < block; f++) { - const float v = o[f]; - if (!isfinite(v)) win_nonfinite = 1; - else win_sum += (double)v * (double)v; - } - win_n += block; - if (win_nonfinite) nonfinite_windows++; - } - if ((b + 1) % win_blocks == 0) { - const double rms = win_n ? sqrt(win_sum / (double)win_n) : 0.0; - if (first_rms < 0 && windows >= 2) first_rms = rms; /* skip settling */ - last_rms = rms; - if (rms < min_rms) min_rms = rms; - if (rms > max_rms) max_rms = rms; - /* Dead relative to what this plugin was doing at the start: a ratio. */ - if (first_rms > 0 && rms > 0 && 20.0 * log10(rms / first_rms) < -40.0) silent_windows++; - else if (first_rms > 0 && rms == 0.0) silent_windows++; - const long r = rss_kb(); - if (r > rss_max) rss_max = r; - windows++; - win_sum = 0.0; - win_n = 0; - } - } - lilv_instance_deactivate(si); - lilv_instance_free(si); - printf("{\"uri\":"); - json_str(stdout, uri); - printf(",\"soak\":{\"instantiated\":true,\"seconds\":%.0f,\"rate\":%.0f,\"block\":%u" - ",\"windows\":%ld,\"nonFiniteWindows\":%ld,\"silentWindows\":%ld" - ",\"firstRmsDbfs\":%.2f,\"lastRmsDbfs\":%.2f,\"minRmsDbfs\":%.2f" - ",\"rssStartKb\":%ld,\"rssMaxKb\":%ld,\"rssGrowthKb\":%ld}}\n", - soak_s, rate, block, windows, nonfinite_windows, silent_windows, - first_rms > 0 ? 20.0 * log10(first_rms) : -999.0, - last_rms > 0 ? 20.0 * log10(last_rms) : -999.0, - min_rms < 1e29 && min_rms > 0 ? 20.0 * log10(min_rms) : -999.0, - rss_start, rss_max, rss_max - rss_start); - free(ab); - free(cb); - for (uint32_t i = 0; i < n_ports; i++) free(atb[i]); - free(atb); - return 0; - } - - /* ---- lifecycle: instantiate and destroy, counting what does not come back ---- */ - const int threads_before = count_threads(); - int instantiated = 0, failed = 0; - int threads_after_first = -1; - for (int c = 0; c < cycles; c++) { - LilvInstance *inst = lilv_plugin_instantiate(plugin, rate, features); - if (inst == NULL) { - failed++; - continue; - } - lilv_instance_activate(inst); - if (c == 0) threads_after_first = count_threads(); - lilv_instance_deactivate(inst); - lilv_instance_free(inst); - instantiated++; - } - const int threads_after = count_threads(); - - if (instantiated == 0) { - printf("{\"uri\":"); - json_str(stdout, uri); - printf(",\"found\":true,\"instantiated\":false,\"cyclesAttempted\":%d}\n", cycles); - return 2; - } - - /* ---- rt-safety: repeated, worst-case ---- */ - uint64_t allocs = 0, locks = 0, syscalls = 0; - uint64_t allocs_min = UINT64_MAX, locks_min = UINT64_MAX, syscalls_min = UINT64_MAX; - int swept = 0; - struct omx_rt_event seen[OMX_RT_EV_MAX]; - int n_seen = 0; - for (int rep = 0; rep < repeats; rep++) { - LilvInstance *inst = lilv_plugin_instantiate(plugin, rate, features); - uint64_t r_allocs = 0, r_locks = 0, r_syscalls = 0; - if (inst != NULL) { - /* Every port gets the buffer type it actually expects. An atom port handed a - * float* is a segfault, not a measurement — LSP's equalisers carry two of them - * beside 196 control ports, which is how this was found. */ - const size_t audio_slots = (size_t)n_audio_in + n_audio_out + n_cv + 2; - float *audio = calloc(audio_slots * block, sizeof(float)); - float *controls = calloc(n_ports, sizeof(float)); - void **atom_bufs = calloc(n_ports, sizeof(void *)); - uint32_t ai = 0, ao = 0; - for (uint32_t i = 0; i < n_ports; i++) { - const LilvPort *port = lilv_plugin_get_port_by_index(plugin, i); - const int is_in = lilv_port_is_a(plugin, port, input_class); - if (lilv_port_is_a(plugin, port, audio_class) || lilv_port_is_a(plugin, port, cv_class)) { - size_t slot = is_in ? ai++ : (n_audio_in + n_cv + ao++); - if (slot >= audio_slots) slot = audio_slots - 1; - lilv_instance_connect_port(inst, i, audio + slot * block); - } else if (lilv_port_is_a(plugin, port, control_class)) { - controls[i] = isnan(defs[i]) ? 0.0f : defs[i]; - lilv_instance_connect_port(inst, i, controls + i); - } else { - /* Anything else — atom, event, MIDI — gets a real, correctly headed buffer. - * An input sequence is empty but VALID (v1 feeds no events); an output - * sequence advertises its capacity so the plugin can write into it. */ - const size_t cap = 8192; - uint8_t *buf = calloc(1, cap); - atom_bufs[i] = buf; - LV2_Atom_Sequence *seq = (LV2_Atom_Sequence *)buf; - seq->atom.size = is_in ? (uint32_t)sizeof(LV2_Atom_Sequence_Body) - : (uint32_t)(cap - sizeof(LV2_Atom)); - seq->atom.type = 0; /* no URID map here: an unmapped type is inert, not a lie */ - seq->body.unit = 0; - seq->body.pad = 0; - lilv_instance_connect_port(inst, i, buf); - } - } - /* A -20 dBFS tone, so the plugin has real signal rather than silence: a gate or - * an expander given silence never leaves its quiet branch and would measure as - * RT-clean while its loud path allocates. */ - for (uint32_t i = 0; i < n_audio_in; i++) { - for (uint32_t f = 0; f < block; f++) { - if (use_noise) { - rng = rng * 1103515245u + 12345u; - audio[i * block + f] = 0.1f * ((float)((rng >> 16) & 0x7fff) / 16384.0f - 1.0f); - } else { - audio[i * block + f] = 0.1f * sinf(2.0f * (float)M_PI * 1000.0f * (float)f / (float)rate); - } - } - } - - lilv_instance_activate(inst); - for (int b = 0; b < warmup_blocks; b++) lilv_instance_run(inst, block); - - if (rt_reset) rt_reset(); - for (int b = 0; b < measured_blocks; b++) { - /* Sweep every control input across its declared range as the blocks go by, so - * the counted window covers the settings an operator will actually reach. */ - const float t = (float)b / (float)measured_blocks; - for (uint32_t i = 0; i < n_ports; i++) { - const LilvPort *port = lilv_plugin_get_port_by_index(plugin, i); - if (!lilv_port_is_a(plugin, port, control_class)) continue; - if (!lilv_port_is_a(plugin, port, input_class)) continue; - if (isnan(mins[i]) || isnan(maxes[i]) || maxes[i] <= mins[i]) continue; - controls[i] = mins[i] + (maxes[i] - mins[i]) * t; - swept = 1; - } - if (rt_begin) rt_begin(); - lilv_instance_run(inst, block); - if (rt_end) rt_end(); - } - if (rt_counts) rt_counts(&r_allocs, &r_locks, &r_syscalls); - if (rt_events && n_seen == 0) n_seen = rt_events(seen, OMX_RT_EV_MAX); - lilv_instance_deactivate(inst); - lilv_instance_free(inst); - free(audio); - free(controls); - for (uint32_t i = 0; i < n_ports; i++) free(atom_bufs[i]); - free(atom_bufs); - } - if (r_allocs > allocs) allocs = r_allocs; - if (r_locks > locks) locks = r_locks; - if (r_syscalls > syscalls) syscalls = r_syscalls; - if (r_allocs < allocs_min) allocs_min = r_allocs; - if (r_locks < locks_min) locks_min = r_locks; - if (r_syscalls < syscalls_min) syscalls_min = r_syscalls; - } - if (allocs_min == UINT64_MAX) allocs_min = 0; - if (locks_min == UINT64_MAX) locks_min = 0; - if (syscalls_min == UINT64_MAX) syscalls_min = 0; - - /* A plugin with no sweepable control was still measured; saying it was swept would - * be a lie the classifier would act on. Report what happened. */ - const int sweep_applicable = n_control_in > 0; - - printf("{\"uri\":"); - json_str(stdout, uri); - printf(",\"found\":true,\"instantiated\":true"); - printf(",\"rate\":%.0f,\"block\":%u", rate, block); - printf(",\"audioInputs\":%u,\"audioOutputs\":%u,\"cvPorts\":%u,\"controlInputs\":%u", - n_audio_in, n_audio_out, n_cv, n_control_in); - /* the pin (plugin-qualify spec §11): the files this world loaded the plugin from, and its layout */ - omx_lv2_pin_print(stdout, world, plugin); - - printf(",\"requiredFeatures\":["); - int first = 1; - LILV_FOREACH (nodes, i, required) { - const LilvNode *n = lilv_nodes_get(required, i); - if (!first) printf(","); - json_str(stdout, lilv_node_as_string(n)); - first = 0; - } - printf("],\"optionalFeatures\":["); - first = 1; - LILV_FOREACH (nodes, i, optional) { - const LilvNode *n = lilv_nodes_get(optional, i); - if (!first) printf(","); - json_str(stdout, lilv_node_as_string(n)); - first = 0; - } - printf("]"); - - printf(",\"lifecycle\":{\"cycles\":%d,\"instantiated\":%d,\"failed\":%d}", - cycles, instantiated, failed); - printf(",\"threads\":{\"before\":%d,\"afterFirstInstantiate\":%d,\"afterAllFreed\":%d,\"leaked\":%d}", - threads_before, threads_after_first, threads_after, - threads_after > threads_before ? threads_after - threads_before : 0); - printf(",\"rtSafety\":{\"interposer\":%s,\"blocks\":%d,\"repeats\":%d,\"swept\":%s," - "\"sweepApplicable\":%s,\"allocationsInRun\":%llu,\"allocationsMin\":%llu," - "\"variable\":%s,\"locksInRun\":%llu,\"locksMin\":%llu," - "\"syscallsInRun\":%llu,\"syscallsMin\":%llu,\"events\":[", - (rt_present && rt_present()) ? "true" : "false", - measured_blocks, repeats, swept ? "true" : "false", sweep_applicable ? "true" : "false", - (unsigned long long)allocs, (unsigned long long)allocs_min, - (allocs != allocs_min || locks != locks_min || syscalls != syscalls_min) ? "true" : "false", - (unsigned long long)locks, (unsigned long long)locks_min, - (unsigned long long)syscalls, (unsigned long long)syscalls_min); - /* The first events, attributed. Deduplicated by (kind, module, symbol) so a plugin that - * mallocs 600 000 times reports one line, and one that does three different things - * reports three. */ - { - const char *dk[OMX_RT_EV_MAX], *dm[OMX_RT_EV_MAX], *ds[OMX_RT_EV_MAX]; - int nd = 0; - for (int i = 0; i < n_seen; i++) { - const char *mod, *sym; - rt_owner(&seen[i], &mod, &sym); - const char *kind = rt_kind_name(seen[i].kind); - int dup = 0; - for (int j = 0; j < nd; j++) - if (!strcmp(dk[j], kind) && !strcmp(dm[j], mod) && !strcmp(ds[j], sym)) { dup = 1; break; } - if (dup) continue; - dk[nd] = kind; dm[nd] = mod; ds[nd] = sym; - printf("%s{\"kind\":\"%s\",\"module\":", nd ? "," : "", kind); - json_str(stdout, mod); - printf(",\"symbol\":"); - json_str(stdout, sym); - printf("}"); - nd++; - } - } - printf("]}"); - - /* ---- rate support: instantiate and RUN at each rate the console can boot at ---- - * Instantiating is not enough. AmpVTS instantiates happily at 96 kHz and then - * outputs digital silence, which is why this runs blocks and checks the output is - * finite and non-silent for a plugin that was given signal. A plugin that dies or - * goes quiet at a rate must not be offered at that rate. The rates are the declared ones - * (OMX_DECLARED_RATES, generated from core's STANDARD_SAMPLE_RATES). */ - printf(",\"rates\":{"); - for (size_t ri = 0; ri < OMX_DECLARED_RATE_COUNT; ri++) { - const double r = OMX_DECLARED_RATES[ri]; - int ok = 0, silent = 0, nonfinite = 0; - double lat_frames = -1.0, rms_out = -1.0; - omx_lv2_options_build(&options, r, block); /* the options name the rate it is instantiated at */ - LilvInstance *pi = lilv_plugin_instantiate(plugin, r, features); - if (pi != NULL) { - const size_t slots = (size_t)n_audio_in + n_audio_out + n_cv + 2; - float *ab = calloc(slots * block, sizeof(float)); - float *cb = calloc(n_ports, sizeof(float)); - void **atb = calloc(n_ports, sizeof(void *)); - uint32_t pai = 0, pao = 0; - for (uint32_t i = 0; i < n_ports; i++) { - const LilvPort *port = lilv_plugin_get_port_by_index(plugin, i); - const int is_in = lilv_port_is_a(plugin, port, input_class); - if (lilv_port_is_a(plugin, port, audio_class) || lilv_port_is_a(plugin, port, cv_class)) { - size_t slot = is_in ? pai++ : (n_audio_in + n_cv + pao++); - if (slot >= slots) slot = slots - 1; - lilv_instance_connect_port(pi, i, ab + slot * block); - } else if (lilv_port_is_a(plugin, port, control_class)) { - cb[i] = isnan(defs[i]) ? 0.0f : defs[i]; - lilv_instance_connect_port(pi, i, cb + i); - } else { - const size_t cap = 8192; - uint8_t *b = calloc(1, cap); - atb[i] = b; - LV2_Atom_Sequence *sq = (LV2_Atom_Sequence *)b; - sq->atom.size = is_in ? (uint32_t)sizeof(LV2_Atom_Sequence_Body) - : (uint32_t)(cap - sizeof(LV2_Atom)); - lilv_instance_connect_port(pi, i, b); - } - } - lilv_instance_activate(pi); - double sum = 0.0; - const char *pb = getenv("OMX_PROBE_BLOCKS"); - const int probe_blocks = pb ? atoi(pb) : 96; - const int probe_settle = probe_blocks / 3; - for (int b2 = 0; b2 < probe_blocks; b2++) { - for (uint32_t i = 0; i < n_audio_in; i++) { - for (uint32_t f = 0; f < block; f++) { - if (use_noise) { - rng = rng * 1103515245u + 12345u; - ab[i * block + f] = 0.1f * ((float)((rng >> 16) & 0x7fff) / 16384.0f - 1.0f); - } else { - ab[i * block + f] = - 0.1f * sinf(2.0f * (float)M_PI * 1000.0f * (float)(b2 * block + f) / (float)r); - } - } - } - lilv_instance_run(pi, block); - if (b2 >= probe_settle && n_audio_out > 0) { - const float *o = ab + (size_t)(n_audio_in + n_cv) * block; - for (uint32_t f = 0; f < block; f++) { - const float v = o[f]; - if (!isfinite(v)) nonfinite = 1; - else sum += (double)v * (double)v; - } - } - } - /* The plugin's own reported latency, if it publishes one. */ - for (uint32_t i = 0; i < n_ports; i++) { - const LilvPort *port = lilv_plugin_get_port_by_index(plugin, i); - if (!lilv_port_is_a(plugin, port, control_class)) continue; - if (!lilv_port_is_a(plugin, port, output_class)) continue; - LilvNode *dn = lilv_port_get_name(plugin, port); - if (dn && strcasestr(lilv_node_as_string(dn), "latency")) lat_frames = cb[i]; - if (dn) lilv_node_free(dn); - } - rms_out = n_audio_out ? sqrt(sum / ((double)(probe_blocks - probe_settle) * block)) : -1.0; - /* An ABSOLUTE silence threshold cannot see AmpVTS: it reads 1.07e-5 at 96 kHz, - * quiet enough to be useless and loud enough to pass any fixed floor. The - * verdict is a RATIO against the plugin's own best rate, computed by the - * caller from the per-rate rms this emits. Reported here, judged there. */ - silent = 0; - ok = 1; - lilv_instance_deactivate(pi); - lilv_instance_free(pi); - free(ab); - free(cb); - for (uint32_t i = 0; i < n_ports; i++) free(atb[i]); - free(atb); - } - printf("%s\"%.0f\":{\"instantiated\":%s,\"nonFinite\":%s,\"latencyFrames\":%.0f," - "\"rms\":%.9g,\"rmsDbfs\":%.2f}", - ri ? "," : "", r, ok ? "true" : "false", - nonfinite ? "true" : "false", lat_frames, rms_out, - rms_out > 0 ? 20.0 * log10(rms_out) : -999.0); - (void)silent; - } - printf("}"); - - printf("}\n"); - - free(mins); - free(maxes); - free(defs); - lilv_nodes_free(required); - lilv_nodes_free(optional); - lilv_node_free(audio_class); - lilv_node_free(control_class); - lilv_node_free(input_class); - lilv_node_free(output_class); - lilv_node_free(cv_class); - lilv_node_free(uri_node); - lilv_world_free(world); - return 0; -} diff --git a/native/parity/openmixer/rt-interpose.h b/native/parity/openmixer/rt-interpose.h deleted file mode 100644 index 6c06e9a..0000000 --- a/native/parity/openmixer/rt-interpose.h +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2026 Pau Aliagas -/* The event record rt-interpose.so keeps for the host: what a plugin did inside run(), and - * from where. A count says that something happened; this says what, which is what a ruling - * on a count of 1 needs. Shared by the interposer and lv2-measure, identical on both sides. */ -#pragma once -#define OMX_RT_EV_MAX 16 -#define OMX_RT_EV_DEPTH 12 -enum omx_rt_kind { - OMX_RT_MALLOC = 1, OMX_RT_CALLOC, OMX_RT_REALLOC, OMX_RT_FREE, - OMX_RT_MUTEX_LOCK, OMX_RT_MUTEX_TRYLOCK, - OMX_RT_OPEN, OMX_RT_NANOSLEEP, OMX_RT_WRITE, OMX_RT_READ, -}; -struct omx_rt_event { int kind; int depth; void *frames[OMX_RT_EV_DEPTH]; }; diff --git a/packaging/check-installed.sh b/packaging/check-installed.sh index f2d35df..54cfb16 100755 --- a/packaging/check-installed.sh +++ b/packaging/check-installed.sh @@ -13,7 +13,7 @@ # fixture must qualify (exit 0, suitable), the unsafe one must not (exit 1, unsuitable), and the # installed prober must print the fixture's integrity pin. The installed lv2-measure.mjs must measure # the LV2 fixtures' hosting dimensions through the installed measurer: no allocation in the safe -# one's run(), some in the unsafe one's, each with its pin. +# one's run(), some in the unsafe one's, each with its pin. The installed LV2 host probe must run. set -euo pipefail usage="usage: check-installed.sh " @@ -60,3 +60,14 @@ measure=$( (dpkg -L plugin-qualify 2>/dev/null || rpm -ql plugin-qualify) | grep fail "lv2-measure.mjs failed on the LV2 fixtures" bare node packaging/check-lv2-measurement.mjs "$out/lv2.json" || fail "the LV2 measurement is not the fixtures'" echo "check-installed: $dir measures LV2 plugins with no setting" + +# the LV2 host probe a console runs on the plugin it exports: installed, and it tells a plugin lilv +# cannot find (exit 2) from one that failed a claim (exit 1) +[ -x "$dir/lv2_host_probe" ] || fail "the package installs no lv2_host_probe" +set +e +bare "$dir/lv2_host_probe" "$lv2/rt-safe.lv2" urn:plugin-qualify:fixture:no-such-plugin > "$out/probe.txt" 2>&1 +rc=$? +set -e +[ "$rc" -eq 2 ] || fail "lv2_host_probe on a plugin the bundle lacks exited $rc, not 2" +grep -q 'could not find urn:plugin-qualify:fixture:no-such-plugin' "$out/probe.txt" || fail "lv2_host_probe did not name the plugin it could not find" +echo "check-installed: $dir carries the LV2 host probe" diff --git a/src/lv2-measure-parity.test.ts b/src/lv2-measure-parity.test.ts deleted file mode 100644 index ee99755..0000000 --- a/src/lv2-measure-parity.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// Copyright (C) 2026 Pau Aliagas -/** - * The LV2 measurer moved from openmixer unchanged in behaviour: this build (`native/build/lv2_measure`) - * and openmixer's copy at the commit it was taken from (`native/parity/openmixer`, each file held to its - * git blob id there; built with the same flags into `native/build/parity/lv2_measure` once - * `make -C native deps` has fetched the omx-contract header it reads) print the same bytes and exit - * the same on the fixture - * bundles, in every mode: a measurement at its defaults and at a rate, block and count, the noise - * stimulus, the pin alone, a plugin it cannot find, and the soak. The soak's three RSS figures are the - * process's, not the code's — they move between two runs of one binary — so they are masked. - * - * Since the measuring host was made faithful (`lv2-host-faithfulness.test.ts`) ours prints three things - * openmixer's copy does not, and they alone are taken out of our line before the comparison: the - * `host` object (what the host did: options, worker, patch:Set, the sweep bounds), the - * `featureOptions` the host's features were built under (omx-clap-core's provider, at the measurer's - * defaults here) and the soak's `maxRmsDbfs`. The soak's sweep is bounded now; the soak case lifts the bound (OMX_SWEEP_DB and - * OMX_SWEEP_RATIO far beyond any range) so both sweep the whole range and the levels compare. - * On CI a missing build is a failure; elsewhere the test skips, saying how to build. - */ -import { spawnSync } from "node:child_process"; -import { createHash } from "node:crypto"; -import { existsSync, readFileSync, readdirSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { describe, expect, it } from "vitest"; - -const NATIVE = join(dirname(fileURLToPath(import.meta.url)), "..", "native"); -const BUILD = join(NATIVE, "build"); -const SNAPSHOT = join(NATIVE, "parity", "openmixer"); -const OURS = join(BUILD, "lv2_measure"); -const THEIRS = join(BUILD, "parity", "lv2_measure"); -const INTERPOSER = join(BUILD, "rt_interpose.so"); -const LV2_FIXTURES = join(BUILD, "fixtures", "lv2"); -const SAFE = "urn:plugin-qualify:fixture:lv2-rt-safe"; -const UNSAFE = "urn:plugin-qualify:fixture:lv2-rt-unsafe"; -const BUILT = [OURS, THEIRS, INTERPOSER, join(LV2_FIXTURES, "rt-unsafe.lv2", "gain.so")].every((p) => existsSync(p)); - -interface Case { - readonly name: string; - readonly args: readonly string[]; - readonly env?: Readonly>; - readonly mask?: (stdout: string) => string; -} - -const RSS = /"(rssStartKb|rssMaxKb|rssGrowthKb)":-?\d+/g; -/** What only ours prints: the host's facts, the feature options, and the soak's loudest window. */ -const FEATURE_OPTIONS = /,"featureOptions":\{"features":\[[^\]]*\],"strict":(?:true|false),"block":\{[^{}]*\},"log":\{[^{}]*\}\}/; -const OURS_ONLY = (stdout: string): string => - stdout.replace(/,"host":\{[^{}]*\}/, "").replace(FEATURE_OPTIONS, "").replace(/,"maxRmsDbfs":-?[\d.]+/, ""); -const UNBOUNDED = { OMX_SWEEP_DB: "1e9", OMX_SWEEP_RATIO: "1e9" }; - -const CASES: readonly Case[] = [SAFE, UNSAFE].flatMap((uri): Case[] => [ - { name: `${uri} at the defaults`, args: [uri] }, - { name: `${uri} at 96 kHz, 128 frames, 5 lifecycles, 2 repeats`, args: [uri, "96000", "128", "5", "2"] }, - { name: `${uri} under noise`, args: [uri, "48000", "256", "3", "3"], env: { OMX_STIM: "noise", OMX_PROBE_BLOCKS: "30" } }, - { name: `${uri}, the pin alone`, args: ["--pin", uri] }, - { name: `${uri} soaked for a second`, args: [uri, "48000", "512"], env: { OMX_SOAK_SECONDS: "1", ...UNBOUNDED }, mask: (s) => s.replace(RSS, '"$1":N') }, -]).concat([{ name: "a plugin it cannot find", args: ["urn:plugin-qualify:not-here"] }]); - -function run(tool: string, c: Case): { status: number | null; stdout: string } { - const r = spawnSync(tool, [...c.args], { - encoding: "utf8", - env: { PATH: process.env["PATH"], LV2_PATH: LV2_FIXTURES, LD_PRELOAD: INTERPOSER, ...c.env }, - }); - return { status: r.status, stdout: c.mask === undefined ? r.stdout : c.mask(r.stdout) }; -} - -describe("openmixer's copy is kept as it was there", () => { - it("every file is the blob BLOBS records, and BLOBS records every file", () => { - const blob = (path: string): string => { - const body = readFileSync(path); - return createHash("sha1").update(`blob ${body.length}\0`).update(body).digest("hex"); - }; - const recorded = readFileSync(join(SNAPSHOT, "BLOBS"), "utf8") - .split("\n") - .filter((l) => /^[0-9a-f]{40} {2}\S+$/.test(l)) - .map((l) => [l.slice(42), l.slice(0, 40)] as const); - expect(recorded.map(([f]) => f).sort()).toEqual(readdirSync(SNAPSHOT).filter((f) => f !== "BLOBS").sort()); - for (const [file, id] of recorded) expect(blob(join(SNAPSHOT, file)), file).toBe(id); - }); -}); - -describe("lv2-measure prints what openmixer's copy prints, byte for byte", () => { - for (const c of CASES) { - it(c.name, (ctx) => { - if (!BUILT) { - if (process.env["CI"]) throw new Error("native/build/parity is missing: CI builds it with make -C native deps all"); - ctx.skip("native/build/parity is missing: make -C native deps all"); - } - const ours = { ...run(OURS, c) }; - ours.stdout = OURS_ONLY(ours.stdout); - const theirs = run(THEIRS, c); - expect(ours.stdout).toMatch(/^\{"uri":/); - expect(ours.stdout).toBe(theirs.stdout); - expect(ours.status).toBe(theirs.status); - }); - } -}); diff --git a/src/qualify-declarations.ts b/src/qualify-declarations.ts index 27d28b9..c9e2123 100644 --- a/src/qualify-declarations.ts +++ b/src/qualify-declarations.ts @@ -82,18 +82,22 @@ export const CLAP_SYSTEM_DIR = "/usr/lib/clap"; export const CLAP_SCAN_TIMEOUT_S = 300; /** - * The CLAP prober (`native/clap-qualify.c`), the LV2 measurer (`native/lv2-measure.c`) and the - * interposer both run under (`native/rt-interpose.c`): each located by one environment variable, else - * the package's own build in `native/build/`, else the first of `NATIVE_TOOL_DIRS` that holds it. + * The CLAP prober (`native/clap-qualify.c`), the LV2 measurer (`native/lv2-measure.c`), the + * interposer both run under (`native/rt-interpose.c`) and the LV2 host probe + * (`native/lv2-host-probe.c`, a worker plugin driven through lilv as a host drives it, for a console + * that exports an LV2 plugin to test its host-facing half): each located by one environment variable, + * else the package's own build in `native/build/`, else the first of `NATIVE_TOOL_DIRS` that holds it. */ export const CLAP_QUALIFY_TOOL_ENV = "OMX_CLAP_QUALIFY"; export const LV2_MEASURE_TOOL_ENV = "OMX_LV2_MEASURE"; export const RT_INTERPOSE_ENV = "OMX_RT_INTERPOSE"; +export const LV2_HOST_PROBE_TOOL_ENV = "OMX_LV2_HOST_PROBE"; /** The file names of the native tools, in every place they are looked for. */ export const CLAP_QUALIFY_TOOL_NAME = "clap-qualify"; export const LV2_MEASURE_TOOL_NAME = "lv2_measure"; export const RT_INTERPOSE_NAME = "rt_interpose.so"; +export const LV2_HOST_PROBE_TOOL_NAME = "lv2_host_probe"; /** * The ONE list of the native tools the packages install, each with the setting that names it. @@ -103,6 +107,7 @@ export const NATIVE_TOOLS: readonly { readonly name: string; readonly env: strin { name: CLAP_QUALIFY_TOOL_NAME, env: CLAP_QUALIFY_TOOL_ENV }, { name: LV2_MEASURE_TOOL_NAME, env: LV2_MEASURE_TOOL_ENV }, { name: RT_INTERPOSE_NAME, env: RT_INTERPOSE_ENV }, + { name: LV2_HOST_PROBE_TOOL_NAME, env: LV2_HOST_PROBE_TOOL_ENV }, ]; /**