[{"data":1,"prerenderedAt":57},["ShallowReactive",2],{"project-omx-panel-lv2-complement":3},{"page":4,"others":35},{"slug":5,"src":6,"title":7,"source":8,"toc":9,"html":34},"lv2-complement","docs\u002Flv2-complement.md","The LV2 complement: a replacing bundle with the inferred port groups","https:\u002F\u002Fgithub.com\u002FFreeMixer\u002Fomx-panel\u002Fblob\u002Fedaf4f48de7e57496ead1b949f754b7e111867e3\u002Fdocs\u002Flv2-complement.md",[10,13,16,19,22,25,28,31],{"id":11,"title":12},"why-a-replacing-bundle","Why a replacing bundle",{"id":14,"title":15},"what-is-written","What is written",{"id":17,"title":18},"what-is-not-written","What is not written",{"id":20,"title":21},"what-is-removed","What is removed",{"id":23,"title":24},"where-it-runs-and-whether-lilv-sees-it","Where it runs, and whether lilv sees it",{"id":26,"title":27},"the-search-order-the-same-result-not-the-same-cost","The search order: the same result, not the same cost",{"id":29,"title":30},"measured","Measured",{"id":32,"title":33},"open-for-review","Open for review","\u003Ch1 id=\"the-lv2-complement-a-replacing-bundle-with-the-inferred-port-groups\">The LV2 complement: a replacing bundle with the inferred port groups\u003C\u002Fh1>\n\u003Cp>\u003Ccode>tools\u002Fgroup-infer.mjs\u003C\u002Fcode> (\u003Ca href=\"\u002Fprojects\u002Fomx-panel\u002Fgroup-inference\">group-inference.md\u003C\u002Fa>) infers the port groups an LV2 plugin lacks.\n\u003Ccode>tools\u002Flv2-complement.mjs\u003C\u002Fcode> (\u003Ccode>omx-panel complement\u003C\u002Fcode>, run at install time by \u003Ccode>omx-complement-sync\u003C\u002Fcode>) carries them to\nthe programs that read plugins through lilv, the Zynthian touchscreen's jalv first: plugin-hostd#218, step G2.\u003C\u002Fp>\n\u003Cpre>\u003Ccode>omx-panel complement --out &lt;dir&gt; [--lv2-path &lt;a:b:...&gt;] [--state &lt;dir&gt;] [--only &lt;bundle.lv2&gt;]... [--dry-run | --check] [--verbose]\nomx-complement-sync [--dry-run] [--verbose] | --check\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"why-a-replacing-bundle\">Why a replacing bundle\u003C\u002Fh2>\n\u003Cp>A second bundle that only adds \u003Ccode>pg:group\u003C\u002Fcode> to a plugin cannot work: a plugin's ports are blank nodes, local to the file\nthat states them, so another file has nothing to attach a group to. What lilv does offer is its duplicate rule: a\nbundle that declares a plugin already loaded \u003Cstrong>replaces\u003C\u002Fstrong> it when its \u003Ccode>lv2:minorVersion\u003C\u002Fcode>\u002F\u003Ccode>lv2:microVersion\u003C\u002Fcode> is newer,\nand is dropped when it is equal or older (lilv \u003Ccode>world.c\u003C\u002Fcode>, \u003Ccode>lilv_world_load_bundle\u003C\u002Fcode>; the order of the search list does\nnot change which bundle wins, it is a comparison; it changes what the load costs, \u003Ca href=\"#the-search-order-the-same-result-not-the-same-cost\">below\u003C\u002Fa>). lilv reads a version only when both values are stated, with \u003Ccode>atoi\u003C\u002Fcode>; otherwise it is\n0.0.\u003C\u002Fp>\n\u003Cp>lilv drops the \u003Cstrong>whole\u003C\u002Fstrong> old bundle when one of its plugins is replaced (&quot;Dropping old bundle&quot;): replacing MDA's\nRezFilter alone left one of MDA's 36 plugins listed. So the unit the writer works on is the installed bundle, not the\nplugin. For a bundle with one plugin to group it writes a bundle that restates every plugin of it, each one bumped;\nthe groups go to the grouped plugins only, the others are restated unchanged.\u003C\u002Fp>\n\u003Ch2 id=\"what-is-written\">What is written\u003C\u002Fh2>\n\u003Ch3 id=\"the-name\">The name\u003C\u002Fh3>\n\u003Cp>\u003Ccode>omx-complement-&lt;name&gt;.lv2\u003C\u002Fcode>, \u003Ccode>&lt;name&gt;\u003C\u002Fcode> the bundle's basename without \u003Ccode>.lv2\u003C\u002Fcode>, characters outside \u003Ccode>A-Za-z0-9._-\u003C\u002Fcode>\nreplaced by \u003Ccode>_\u003C\u002Fcode>. When another installed bundle would take the same name (one basename in two folders of the search\nlist, or basenames that differ only in the characters replaced), every bundle sharing it is named\n\u003Ccode>omx-complement-&lt;name&gt;-&lt;hash&gt;.lv2\u003C\u002Fcode> instead, \u003Ccode>&lt;hash&gt;\u003C\u002Fcode> the first eight hex digits of the SHA-256 of the installed\nbundle's absolute path. The hash depends on the path alone, so the name is stable between runs, and both bundles are\nwritten: a name of ours that collides is our naming problem, never a reason to refuse a plugin. The marker names the\ninstalled bundle, so the stale-bundle removal below is exact on the derived name. A bundle whose basename is alone\nkeeps the plain name; when a second bundle of that basename appears (or the other goes), the plain and the derived\nnames are swapped by the ordinary removal of the bundle of ours that is no longer wanted.\u003C\u002Fp>\n\u003Cp>For an installed bundle \u003Ccode>&lt;dir&gt;\u002F&lt;name&gt;.lv2\u003C\u002Fcode> holding at least one plugin whose verdict is \u003Ccode>grouped\u003C\u002Fcode>, the bundle\n\u003Ccode>&lt;out&gt;\u002Fomx-complement-&lt;name&gt;.lv2\u003C\u002Fcode> holds:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\n\u003Cp>\u003Cstrong>every TTL file lilv reads for it\u003C\u002Fstrong>, restated: \u003Ccode>manifest.ttl\u003C\u002Fcode> and each file a plugin's \u003Ccode>rdfs:seeAlso\u003C\u002Fcode> names there\n(what lilv's \u003Ccode>load_plugin_model\u003C\u002Fcode> reads). Each file is parsed by \u003Ccode>tools\u002Fturtle-read.mjs\u003C\u002Fcode> and written back statement\nfor statement: every IRI as the reader resolved it (absolute), every literal with its own lexical form and datatype\nor language (\u003Ccode>+30\u003C\u002Fcode>, \u003Ccode>0.0\u003C\u002Fcode>, \u003Ccode>1e-05\u003C\u002Fcode> stay as written; a number is written as a number only when the reader would read\nit back as the same lexical form and type), every blank node under a label prefixed by its file (\u003Ccode>_:f1_b12\u003C\u002Fcode>): lilv\nkeeps each file's blank nodes apart, a tool that reads all the files into one graph (sord_validate) does not.\nNothing is retyped and nothing is left out; comments are not statements and are not carried.\u003C\u002Fp>\n\u003C\u002Fli>\n\u003Cli>\n\u003Cp>\u003Cstrong>\u003Ccode>rdfs:seeAlso\u003C\u002Fcode> pointing at the restated files\u003C\u002Fstrong> (written relative, \u003Ccode>&lt;plugin.ttl&gt;\u003C\u002Fcode>), so lilv reads ours; any other\n\u003Ccode>rdfs:seeAlso\u003C\u002Fcode> (a preset, a UI) still names the installed file.\u003C\u002Fp>\n\u003C\u002Fli>\n\u003Cli>\n\u003Cp>\u003Cstrong>\u003Ccode>lv2:binary\u003C\u002Fcode> unchanged\u003C\u002Fstrong>: the reader resolved it to the installed library's absolute \u003Ccode>file:\u003C\u002Fcode> IRI, which resolves\nfrom anywhere. Every other entry of the installed bundle (the library, presets, a MOD GUI, resources) is linked in\nby a symbolic link, because lilv hands a plugin its bundle path (ours now) and some plugins read their own files\nfrom there.\u003C\u002Fp>\n\u003C\u002Fli>\n\u003Cli>\n\u003Cp>\u003Cstrong>the version\u003C\u002Fstrong>: each plugin's \u003Ccode>lv2:minorVersion\u003C\u002Fcode> and \u003Ccode>lv2:microVersion\u003C\u002Fcode> statements are dropped and \u003Ccode>manifest.ttl\u003C\u002Fcode>\nstates \u003Ccode>lv2:minorVersion\u003C\u002Fcode> = the installed value (0 when none) and \u003Ccode>lv2:microVersion\u003C\u002Fcode> = the installed value + 1 (1\nwhen none). A plugin stating only a minor version (lilv reads 0.0) keeps its minor.\u003C\u002Fp>\n\u003C\u002Fli>\n\u003Cli>\n\u003Cp>\u003Cstrong>the groups\u003C\u002Fstrong>: for each group of the verdict, a node \u003Ccode>&lt;#&lt;plugin&gt;-&lt;symbol&gt;&gt;\u003C\u002Fcode> in the file that states the plugin's\nports, typed \u003Ccode>pg:Group\u003C\u002Fcode>, with \u003Ccode>lv2:symbol\u003C\u002Fcode>, \u003Ccode>lv2:name\u003C\u002Fcode> and \u003Ccode>lv2:displayPriority\u003C\u002Fcode>, and \u003Ccode>pg:group\u003C\u002Fcode> on each of its\nports, found by \u003Ccode>lv2:symbol\u003C\u002Fcode> in that file. A group the plugin states itself (rule 1) keeps its own node: only what\nthat node lacks of \u003Ccode>lv2:symbol\u003C\u002Fcode>, \u003Ccode>lv2:name\u003C\u002Fcode> and \u003Ccode>lv2:displayPriority\u003C\u002Fcode> is added, and a port that already carries a\n\u003Ccode>pg:group\u003C\u002Fcode> keeps it. A generated node never merges into one already there: \u003Ccode>&lt;#&lt;plugin&gt;-&lt;symbol&gt;&gt;\u003C\u002Fcode> is resolved as the\nTurtle reader resolves it, against the installed file (what the plugin's own \u003Ccode>&lt;#…&gt;\u003C\u002Fcode> of that fragment means) and\nagainst the written file at its target (what lilv will read), and when either IRI is one the bundle already states,\nor another grouped plugin of the same file would take the same node (two plugin URIs ending in the same word), the\nbundle is refused (\u003Ccode>group-iri-collision\u003C\u002Fcode>). Refused rather than given a suffix: a node whose name cannot be predicted\nfrom the plugin and the group symbol would surprise whoever looks it up, and over the 979 plugins on the desk the\nguard refuses none (37 bundles, 84 grouped plugins, as before), so the conservative choice costs nothing measured.\u003C\u002Fp>\n\u003C\u002Fli>\n\u003Cli>\n\u003Cp>\u003Cstrong>the marker\u003C\u002Fstrong> \u003Ccode>.omx-complement-sync\u003C\u002Fcode>, in the bundle, as omx-modgui-sync marks its own with \u003Ccode>.omx-modgui-sync\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002Fusr\u002Flib64\u002Flv2\u002Ftriple_para-swh.lv2\nwriter &lt;sha256 of lv2-complement.mjs, group-infer.mjs and turtle-read.mjs&gt;\nsource-sha256 &lt;sha256 of the installed TTL files read, name and bytes&gt;\nplugin http:\u002F\u002Fplugin.org.uk\u002Fswh-plugins\u002FtriplePara 0.0 0.1 grouped 5\nfile manifest.ttl &lt;sha256&gt;\nfile plugin.ttl &lt;sha256&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The first three lines decide: a bundle whose marker names the same installed bundle, writer and content hash is up\nto date and is not written again (a second run writes nothing, byte for byte the same tree). A changed installed\nfile (an upgrade) or a changed rule rewrites it (\u003Ccode>replaced …\u003C\u002Fcode>, the line saying which changed). A directory of our\nname \u003Cstrong>without\u003C\u002Fstrong> the marker is never written. Deleting the bundle is the whole removal; lilv loads the installed\nbundle again.\u003C\u002Fp>\n\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Ccode>lv2:displayPriority\u003C\u002Fcode> is what zynthian-ui reads on a group node (\u003Ccode>zyngine\u002Fzynthian_lv2.py\u003C\u002Fcode>). It is not in the LV2\nvocabulary (the port-props extension defines \u003Ccode>pprops:displayPriority\u003C\u002Fcode>, for a port), so \u003Ccode>sord_validate\u003C\u002Fcode> reports it as an\nundefined property: over the 37 bundles written on the desk, those are the only messages it gives on ours that it does\nnot give on the installed files.\u003C\u002Fp>\n\u003Ch2 id=\"what-is-not-written\">What is not written\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>A bundle none of whose plugins is \u003Ccode>grouped\u003C\u002Fcode>: a plugin that carries its own groups (our omx-plugins, synthv1, x42\nmatrixmixer), fits one screen, or has no confident grouping is left exactly as it is, no bundle and no marker.\u003C\u002Fli>\n\u003Cli>The installed files: never opened for writing.\u003C\u002Fli>\n\u003Cli>A bundle the restatement cannot carry is \u003Cstrong>refused\u003C\u002Fstrong>, named on its own line with the reason, counted in the summary,\nand never written (step G4 decides what the proxy fallback does with these):\n\u003Cul>\n\u003Cli>\u003Ccode>ttl-unreadable\u003C\u002Fcode>: a file the reader cannot parse (\u003Ccode>ttl-missing\u003C\u002Fcode>: a \u003Ccode>rdfs:seeAlso\u003C\u002Fcode> file that does not exist);\u003C\u002Fli>\n\u003Cli>\u003Ccode>version-ceiling\u003C\u002Fcode>: a version past lilv's \u003Ccode>int\u003C\u002Fcode> (2147483647), or a micro version a bump would take past it;\n\u003Ccode>version-unreadable\u003C\u002Fcode>: a version that is not an integer; \u003Ccode>version-not-newer\u003C\u002Fcode>: a bump that would still not be newer;\u003C\u002Fli>\n\u003Cli>more than one subject for the URI: \u003Ccode>version-ambiguous\u003C\u002Fcode> (two values of \u003Ccode>lv2:minorVersion\u003C\u002Fcode> or \u003Ccode>lv2:microVersion\u003C\u002Fcode>),\n\u003Ccode>binary-ambiguous\u003C\u002Fcode> (two \u003Ccode>lv2:binary\u003C\u002Fcode>; \u003Ccode>no-binary\u003C\u002Fcode> for none), \u003Ccode>plugin-in-several-bundles\u003C\u002Fcode> (the plugin URI declared by\ntwo installed bundles: lilv picks between them by version, and the restatement cannot say which it replaces),\n\u003Ccode>port-symbol-ambiguous\u003C\u002Fcode> (two ports of one symbol);\u003C\u002Fli>\n\u003Cli>\u003Ccode>dynamic-manifest\u003C\u002Fcode>, \u003Ccode>data-outside-bundle\u003C\u002Fcode> (a plugin's \u003Ccode>rdfs:seeAlso\u003C\u002Fcode> outside its bundle), \u003Ccode>ports-split-across-files\u003C\u002Fcode>;\u003C\u002Fli>\n\u003Cli>\u003Ccode>group-iri-collision\u003C\u002Fcode>: a generated group node whose IRI the bundle already states, or that two grouped plugins of\none file would share (above).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>A name taken by two installed bundles is never a refusal (below).\u003C\u002Fp>\n\u003Ch2 id=\"what-is-removed\">What is removed\u003C\u002Fh2>\n\u003Cp>A bundle of ours left in place past its time does harm: its bumped version keeps winning, so it would hide an upgraded\nplugin behind the old restatement, or keep a removed one listed with a library that is gone. So each run, after\nwriting, removes every bundle of ours it did not find up to date or write again, as omx-modgui-sync removes its GUIs,\none line each naming the bundle and why (\u003Ccode>removed &lt;bundle&gt;: &lt;installed bundle&gt;: &lt;reason&gt;\u003C\u002Fcode>):\u003C\u002Fp>\n\u003Cul>\n\u003Cli>its installed bundle is gone, or no longer on the search list (the package was removed);\u003C\u002Fli>\n\u003Cli>its installed bundle changed (the content hash in the marker no longer matches) into one the writer refuses (the\nreason is the refusal's), or that declares no plugin;\u003C\u002Fli>\n\u003Cli>none of the installed bundle's plugins comes out \u003Ccode>grouped\u003C\u002Fcode> on the current rules (the rules changed, or the upgrade\nbrought the plugin's own groups).\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>An installed bundle that changed and still has a plugin to group is written again, which replaces ours whole. Ours is\na directory named \u003Ccode>omx-complement-*.lv2\u003C\u002Fcode> that holds the marker, and only that is ever removed: a directory without the\nmarker, one of another name, or a symbolic link to a marked directory is left alone. With \u003Ccode>--only\u003C\u002Fcode>, only the bundles\nwritten for the bundles named are considered. Once ours is deleted lilv loads the installed bundle again (the test\nproves it through python3-lilv, and \u003Ccode>lv2info\u003C\u002Fcode> shows it, below).\u003C\u002Fp>\n\u003Cp>\u003Ccode>--dry-run\u003C\u002Fcode> prints \u003Ccode>would remove …\u003C\u002Fcode> for each and removes nothing. \u003Ccode>--check\u003C\u002Fcode> writes and removes nothing and names each\nbundle of ours that is \u003Ccode>stale\u003C\u002Fcode> (the sync removes it) or \u003Ccode>out of date\u003C\u002Fcode> (the sync writes it again; a bundle not written\nyet is named \u003Ccode>missing\u003C\u002Fcode> and is not stale); it exits 3 when there is one, 0 when there is none, 1 when it cannot run, 2\non usage.\u003C\u002Fp>\n\u003Ch2 id=\"where-it-runs-and-whether-lilv-sees-it\">Where it runs, and whether lilv sees it\u003C\u002Fh2>\n\u003Cp>\u003Ccode>omx-complement-sync\u003C\u002Fcode> (\u003Ccode>bin\u002F\u003C\u002Fcode>) is omx-modgui-sync's twin and runs beside it: the same shell shape calling omx-panel,\nthe same \u003Ccode>\u002Fetc\u002Fomx\u002Fmodgui-sync.conf\u003C\u002Fcode>, the same directories, the same opt-in unit (\u003Ccode>omx-complement-sync.service\u003C\u002Fcode>,\ninstalled and never enabled) queued by the same drop-in on \u003Ccode>plugin-qualify-sync.service\u003C\u002Fcode> after each scan, the same\n\u003Ccode>--dry-run\u003C\u002Fcode> and \u003Ccode>--check\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>What triggers a run, once the unit is enabled: plugin-qualify's package triggers (rpm \u003Ccode>%transfiletriggerin\u003C\u002Fcode> and\n\u003Ccode>%transfiletriggerpostun\u003C\u002Fcode>, dpkg \u003Ccode>interest-noawait\u003C\u002Fcode>) on \u003Ccode>\u002Fusr\u002Flib\u002Flv2\u003C\u002Fcode>, \u003Ccode>\u002Fusr\u002Flib64\u002Flv2\u003C\u002Fcode> and \u003Ccode>\u002Fusr\u002Flocal\u002Flib\u002Flv2\u003C\u002Fcode> fire\nonce per package transaction that installs, upgrades or removes a file there, and start \u003Ccode>plugin-qualify-sync.service\u003C\u002Fcode>;\nits path unit does the same for a change in those folders (and the ones \u003Ccode>qualify.conf\u003C\u002Fcode> adds) outside a package\nmanager; its timer runs it after boot. When that scan finishes, the drop-in\n(\u003Ccode>plugin-qualify-sync.service.d\u002Fomx-complement-sync.conf\u003C\u002Fcode>) queues \u003Ccode>omx-complement-sync.service\u003C\u002Fcode> from \u003Ccode>ExecStartPost\u003C\u002Fcode>,\nso a removal or an upgrade is followed by the removal or the rewrite of our bundle within the scan's minute of quiet\nplus its run. Two limits: \u003Ccode>ExecStartPost\u003C\u002Fcode> runs only when the scan exits 0 (plugin-qualify-sync exits 0 whatever its\nverdicts and 2 when it cannot run, and then this waits for the next scan); and in a transaction without systemd (an\nimage build, a chroot) the trigger runs the scan directly, with no unit and no drop-in, so the complement follows at the\nnext boot (the unit is \u003Ccode>WantedBy=multi-user.target\u003C\u002Fcode>). Our own writes and removals in \u003Ccode>\u002Fusr\u002Flocal\u002Flib\u002Flv2\u003C\u002Fcode> are changes\nthe path unit sees too: they queue one more scan, after which the complement finds everything up to date and changes\nnothing. It writes its bundles where omx-modgui-sync writes its GUIs: \u003Ccode>MODGUI_DIR\u003C\u002Fcode>, by default\n\u003Ccode>\u002Fusr\u002Flocal\u002Flib\u002Flv2\u003C\u002Fcode> for root (\u003Ccode>~\u002F.lv2\u003C\u002Fcode> for a user); its work files go under \u003Ccode>STATE_DIR\u003C\u002Fcode> (\u003Ccode>\u002Fvar\u002Flib\u002Fomx\u002Fmodgui-sync\u003C\u002Fcode>),\noutside the LV2 path, and each bundle is moved in whole.\u003C\u002Fp>\n\u003Cp>lilv loads the bundles only from its own \u003Ccode>LV2_PATH\u003C\u002Fcode>. On Zynthian, \u003Ccode>\u002Fusr\u002Flocal\u002Flib\u002Flv2\u003C\u002Fcode> is on it: \u003Ccode>zynthian_envars*.sh\u003C\u002Fcode>\n(zynthian-sys) export \u003Ccode>LV2_PATH=\u002Fusr\u002Flib\u002Flv2:\u002Fusr\u002Flib\u002F&lt;arch&gt;\u002Flv2:\u002Fusr\u002Flocal\u002Flib\u002Flv2:...\u003C\u002Fcode>, which zynthian-ui and the\njalv it spawns per plugin inherit, and the mod-host and mod-ui units carry the same list. zynthian-sys gives\nomx-modgui-sync a drop-in that sets that \u003Ccode>LV2_PATH\u003C\u002Fcode>; \u003Ccode>omx-complement-sync.service\u003C\u002Fcode> needs the same drop-in there, so it\nreads the plugins Zynthian loads. \u003Ccode>omx-complement-sync --check\u003C\u002Fcode> says whether \u003Ccode>MODGUI_DIR\u003C\u002Fcode> is on the search list. A\ncomplemented plugin's MOD GUI from omx-modgui-sync still applies: that bundle declares no plugin, only GUI statements\nabout the plugin URI, which lilv merges whatever bundle the plugin itself comes from.\u003C\u002Fp>\n\u003Ch2 id=\"the-search-order-the-same-result-not-the-same-cost\">The search order: the same result, not the same cost\u003C\u002Fh2>\n\u003Cp>Whether \u003Ccode>MODGUI_DIR\u003C\u002Fcode> comes before or after the installed folders on \u003Ccode>LV2_PATH\u003C\u002Fcode>, lilv ends with the same plugins, ours\nin place of the installed ones (the version comparison decides, not the order). What the order changes is the work:\nours first, lilv meets the installed bundle second, finds it older and ignores it; ours last, it loads the installed\nbundle, then meets ours, replaces every plugin of it, drops the old bundle and prints a line for each replacement.\u003C\u002Fp>\n\u003Cp>Measured by the G2 review on the Fedora 44 desk with 979 plugins installed and the 37 bundles written, not\non a Zynthian: \u003Ccode>lv2ls\u003C\u002Fcode> takes 0.01 s over the installed folders alone, 0.03 s with ours first, and 0.37 s with ours\nlast, when lilv also prints 1398 replace lines. Zynthian's \u003Ccode>zynthian_envars*.sh\u003C\u002Fcode> puts \u003Ccode>\u002Fusr\u002Flocal\u002Flib\u002Flv2\u003C\u002Fcode> after the\nsystem folders, so on the touchscreen ours comes last: the costly order. Every program that builds a lilv world pays it\nat start (zynthian-ui, each jalv it spawns, mod-host, mod-ui). Step G4 measures it on the Pi, where the figure that\nmatters is what it adds to jalv's start and to zynthian-ui's plugin scan. Nothing here changes the search order: it is\nZynthian's (and each distribution's) to set, and the result is the same either way.\u003C\u002Fp>\n\u003Ch2 id=\"measured\">Measured\u003C\u002Fh2>\n\u003Cp>Over the 979 plugins installed on a Fedora 44 desk, in 4.8 s: 84 grouped, 471 nothing to do, 424 without\na confident grouping (G1's figures); 37 bundles written, holding 466 plugins (84 of them grouped), none refused, 25 MB\n(LSP's restated bundle is 22 MB, its installed TTL 16.9 MB). With the bundles on \u003Ccode>LV2_PATH\u003C\u002Fcode>, \u003Ccode>lv2ls\u003C\u002Fcode> lists the same 979\nplugins whatever the order of the search list; \u003Ccode>lv2info\u003C\u002Fcode> and python3-lilv load each complemented plugin from ours, at\nthe bumped version, with its groups, and instantiate it from the installed library; deleting a bundle loads the\ninstalled one again. The removal, with \u003Ccode>lv2info\u003C\u002Fcode> over a scratch \u003Ccode>LV2_PATH\u003C\u002Fcode> (ours first, a copy of the installed\n\u003Ccode>triple_para-swh.lv2\u003C\u002Fcode> second): a copy upgraded into one the writer refuses (\u003Ccode>lv2:microVersion &quot;beta&quot;\u003C\u002Fcode>) still loads ours\nat 0.1 until the sync, which names it \u003Ccode>stale\u003C\u002Fcode> under \u003Ccode>--check\u003C\u002Fcode> (exit 3) and then removes it (&quot;it changed, and it is\nrefused now (version-unreadable …)&quot;), after which \u003Ccode>lv2info\u003C\u002Fcode> loads the installed copy with no groups; with the copy\ndeleted (the package removed) \u003Ccode>lv2ls\u003C\u002Fcode> still lists the plugin from ours, its binary gone, until the sync removes ours\n(&quot;its installed bundle … is gone&quot;) and the plugin is no longer listed. The tests (\u003Ccode>test\u002Flv2-complement.test.mjs\u003C\u002Fcode>) build installed-shaped bundles from G1's fixtures\n(\u003Ccode>test\u002Ffixtures\u002Flv2-groups\u003C\u002Fcode>) and prove each of these points, the lilv ones through python3-lilv when it is installed.\u003C\u002Fp>\n\u003Ch2 id=\"open-for-review\">Open for review\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>The group names are G1's: &quot;Group N&quot; when the names share no word, &quot;More&quot; \u002F &quot;More 2&quot; for index runs (x42 stepseq's\nnotes fall to &quot;More&quot;).\u003C\u002Fli>\n\u003Cli>\u003Ccode>lv2:displayPriority\u003C\u002Fcode> versus the vocabulary's \u003Ccode>pprops:displayPriority\u003C\u002Fcode>, above: we write the term the touchscreen\nreads.\u003C\u002Fli>\n\u003Cli>A whole multi-plugin bundle is restated to group a few of its plugins (LSP: 30 of 194). The alternative, refusing\nsuch bundles, would leave most of the grouped plugins without groups.\u003C\u002Fli>\n\u003C\u002Ful>\n",[36,39,42,45,48,51,54],{"slug":37,"title":38},"","omx-panel",{"slug":40,"title":41},"compare","Compare A\u002FB\u002FC\u002FD and undo",{"slug":43,"title":44},"group-inference","Port-group inference for LV2 plugins",{"slug":46,"title":47},"modgui","The MOD GUI generator",{"slug":49,"title":50},"panel-layout","Panel layout from a descriptor",{"slug":52,"title":53},"skin-package","The skin package",{"slug":55,"title":56},"worked-example","Worked example: a third-party LV2 and CLAP plugin to a generated panel, adapted to Zynthian",1793954996622]