mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 02:41:17 +00:00
Merge branch 'main' into pr/tommasobbianchi/15238
This commit is contained in:
@@ -0,0 +1,508 @@
|
||||
# Filament IDs (`filament_id`)
|
||||
|
||||
`filament_id` identifies one **filament product**: one named spool product = one id, shared by
|
||||
all of that product's per-printer / per-nozzle variants, in every profile bundle that ships it.
|
||||
Devices use it to match a physical spool or tray to a filament preset. It is never per-color,
|
||||
per-printer, per-nozzle, or per-preset (per-preset identity is `setting_id`), and it is never
|
||||
per-bundle either — PolyLite PLA carries the same id whether the preset lives in the
|
||||
OrcaFilamentLibrary (OFL), Qidi, or Snapmaker bundle. The granularity is the name on the spool,
|
||||
not the brand behind it: `AAA PLA Lite` and `AAA PLA Pro` are two filaments with two ids, not
|
||||
variants of one.
|
||||
|
||||
**How it is generated:** an id is computed, never invented. `scripts/orca_id_tool.py`
|
||||
mints it as a deterministic hash of the product's identity — the triple
|
||||
`(filament_vendor, filament_type, filament name)`, where the filament name is the preset name
|
||||
with its `@...` variant suffix stripped — producing an 8-character `OF*` code that is the
|
||||
same for that product in every bundle, in every PR, on every machine. For example, Polymaker's
|
||||
PolyLite PLA presets (`PolyLite PLA @base`, `PolyLite PLA@Q2-Series`, …) resolve
|
||||
`filament_vendor` `Polymaker`, `filament_type` `PLA`, and filament name `PolyLite PLA`; hashing
|
||||
`filament_product/Polymaker/PLA/PolyLite PLA` yields `OF5CgdDq`, and that is the id the
|
||||
OrcaFilamentLibrary, OrcaArena, Qidi, and Snapmaker bundles all arrive at independently
|
||||
(derivation details in the Minting section).
|
||||
|
||||
**How it is used:** at runtime the id is the join key between hardware and profiles.
|
||||
When a printer reports what a tray holds (Bambu AMS, Qidi box, Creality CFS,
|
||||
Klipper, Snapmaker), OrcaSlicer matches the reported id against the filament presets
|
||||
compatible with that printer to select the right profile; other features — tray display
|
||||
names, support-material detection, vitrification warnings, multi-nozzle filament grouping —
|
||||
look up material properties by id alone. An id that changes is not forwarded anywhere: a
|
||||
tray or record still holding the old value falls back to matching by material type until the
|
||||
user re-selects the filament, so identity changes are made deliberately and rarely.
|
||||
|
||||
This page is the rule for authoring `filament_id` in system profiles
|
||||
(`resources/profiles/**`). CI enforces everything below; the short version is:
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Never write a `filament_id` value by hand.** A new filament gets its id from
|
||||
> `python scripts/orca_id_tool.py --generate`; one already in the tree has one — inherit it.
|
||||
|
||||
## The design, in two pieces
|
||||
|
||||
Because several consumers match **globally by id alone, first hit wins** (see the next
|
||||
section), any two materials sharing one id feed wrong data somewhere — a wrong tray name, a
|
||||
wrong support-material flag, a wrong nozzle grouping — and inside one printer a duplicated id
|
||||
makes AMS spool matching a coin toss. Hand-written ids produce such collisions constantly, so
|
||||
the system is built to make them impossible:
|
||||
|
||||
1. **Deterministic minting.** An id is a pure hash of the product's identity — no registry to
|
||||
maintain, no next-free-number ceremony, no way for two concurrent PRs to race for the same
|
||||
number, and no way to get it wrong by hand, because you never write it by hand.
|
||||
2. **A sanctioned snapshot.** The complete id landscape derived from the tree must equal
|
||||
`scripts/filament_id_snapshot.json` exactly, so every change to ids, claims (which bundles
|
||||
ship which id, and for which filament), or product identity surfaces as a reviewable diff to
|
||||
one file — the maintainer gate.
|
||||
|
||||
## Who consumes the id
|
||||
|
||||
The canonical consumer is tray-to-preset matching: a device reports a tray material id
|
||||
(`tray_info_idx`), and the shared matching pipeline (`PresetBundle::sync_ams_list` and
|
||||
friends) resolves it to a preset. The matcher is printer-scoped and first-match-wins:
|
||||
scanning only compatible root presets — system roots plus user-made custom filaments, which
|
||||
are user roots carrying their own `P*` ids; a preset derived from another resolves through
|
||||
its root and never matches directly — it picks the first one whose `filament_id` equals the
|
||||
tray's. On a miss it falls back by filament type: a system `Generic <type>` preset
|
||||
(matched by name, then by type similarity), else the slot's previous selection, else any compatible system generic or,
|
||||
failing that, any compatible system preset, else the slot is skipped — every fallback
|
||||
selection surfaces a user-visible notice.
|
||||
|
||||
Today only the Bambu AMS integration follows this pattern end to end — the device itself
|
||||
reports the id, `BBLPrinterAgent` translates it out of Bambu's catalog into ours, and the
|
||||
pipeline does all the matching. The other device integrations still synthesize a preset id
|
||||
client-side in their agents (by type, brand, or color lookups against the loaded presets)
|
||||
before the pipeline runs; they are intended to converge on the same pattern, with the
|
||||
device-reported tray material id flowing through the shared matcher.
|
||||
|
||||
| Ecosystem | Where the tray id comes from today |
|
||||
| --- | --- |
|
||||
| Bambu AMS | the device itself (RFID / user tray setting), in Bambu's own `GF*` catalog; `BBLPrinterAgent` rewrites it into our id before the matcher sees it (see [The Bambu catalog map](#the-bambu-catalog-map)) |
|
||||
| Qidi box | composed at runtime as `QD_<series>_<vendor>_<typeidx>` — vendor and type indices from the device's per-slot saved variables, the series digit inferred client-side from the printer model/name. No preset carries a `QD_*` value, so the slot currently resolves by filament type; mapping the composed id onto the filament's minted id belongs in the agent |
|
||||
| Creality CFS | runtime brand/type scoring returns the winning preset's id |
|
||||
| Klipper (AFC / Happy Hare) | runtime lookup by filament type |
|
||||
| Snapmaker | runtime color/vendor/type match |
|
||||
|
||||
Tray-to-preset matching is printer-scoped, but **several consumers match globally by id alone,
|
||||
first hit wins**: tray display names, `filament_is_support`, vitrification warnings, and
|
||||
multi-nozzle filament grouping in the slicing pipeline (`FilamentGroup::try_merge_filaments`
|
||||
merges plate slots sharing one `(filament_id, color)` pair, with matching
|
||||
extruder-printability, onto one nozzle group; the engine is implemented but no grouping path
|
||||
calls it yet).
|
||||
Two *different* materials sharing one id
|
||||
feed wrong data to those consumers even when the presets live in different vendors — so
|
||||
cross-material id sharing is never safe. Within one printer, duplicate ids break AMS matching:
|
||||
the matcher picks whichever preset loads first (it now logs an "Ambiguous AMS filament match"
|
||||
warning, but the pick is still arbitrary) and the tray-edit dialog, which lists one entry per
|
||||
id, hides the second preset entirely. The profile validator's `-f` check
|
||||
(`check_filament_subtypes` → `PresetBundle::check_duplicate_filament_subtypes`) rejects this
|
||||
per printer, and CI runs it tree-wide.
|
||||
|
||||
Two more consumer-side facts worth knowing:
|
||||
|
||||
- The machine-facing dialogs (AMS tray edit, AMS dry control, calibration history, extrusion
|
||||
calibration) offer the filaments a connected printer can use by the same compatibility rule
|
||||
the plater uses (an empty `compatible_printers` means *every* printer). Alias shadowing
|
||||
still applies: a vendor's same-name profile supersedes the library generic. That is what
|
||||
puts Orca Filament Library materials in those lists — deduplicated to one entry per
|
||||
`filament_id` in the AMS and calibration-history dialogs, while extrusion calibration
|
||||
deliberately lists every matching preset by full name.
|
||||
- The id is load-bearing at startup: an instantiated system filament (one marked
|
||||
`"instantiation": "true"` — see the structure rules) that resolves **no**
|
||||
`filament_id` anywhere in its `inherits` chain is a hard load error in the C++ loader
|
||||
(`Can not find filament_id for <name>`) that discards the entire vendor bundle (for the
|
||||
OrcaFilamentLibrary itself the failure is messier: library presets loaded before the
|
||||
failing one survive, and every vendor bundle whose filaments inherit from the library is
|
||||
then discarded for want of a base). CI's structure check catches this before it ships.
|
||||
|
||||
## Do I need a new id? The one-question test
|
||||
|
||||
> **Would a user consider this a different spool product than anything already in the tree?**
|
||||
|
||||
Different polymer, different sub-brand (Basic / Matte / Silk / HF), fiber-filled sibling, or a
|
||||
second selectable diameter → **new filament, new id**. The same spool tuned for another printer
|
||||
or nozzle → **join the existing filament** (keep its base name and inherit it; no id
|
||||
key needed). Tuning a generic material → **join the OrcaFilamentLibrary filament** (inherit
|
||||
`Generic X @System` and keep the `Generic X` base name; no id key needed).
|
||||
|
||||
| Situation | id |
|
||||
| --- | --- |
|
||||
| Per-printer / per-nozzle variant of an existing material | same id (inherit it) |
|
||||
| Sub-brand or product line (PLA vs PLA Matte vs PLA Silk vs PLA HF) | new id each |
|
||||
| Color | never a new id |
|
||||
| Second diameter of the same product (1.75 + 2.85) | sibling filament, new id |
|
||||
| "High-speed" tuned for a *different printer model* | same id (it is a printer variant) |
|
||||
| "High-speed" selectable *alongside* the normal preset on one printer | new name, so a new id (it is a product line) |
|
||||
|
||||
## Structure rules
|
||||
|
||||
1. **Every preset carries the id of its own product, wherever it gets it from.** The id is a
|
||||
function of the preset's own triple (rule 5), and `inherits` carries settings, never
|
||||
identity. So a preset may declare the key itself or inherit it from any ancestor — a
|
||||
`<Filament> @base` root, a real (instantiated) preset of the same filament, an
|
||||
OrcaFilamentLibrary preset — and CI checks one thing: the id it ends up with equals the
|
||||
mint of *its* triple. The usual shape is one `@base` root (`"instantiation": "false"`)
|
||||
declaring the key and the per-printer variants inheriting it; a filament may have several
|
||||
roots — Qidi's PolyLite PLA has four per-series roots (`PolyLite PLA@Q2-Series`,
|
||||
`@Q2C-Series`, `@X-Max 4-Series`, `@X-Plus 5-Series`) — which then all declare the identical
|
||||
id. A branded filament that borrows a generic's settings (`Flashforge ABS Basic @FF C5`
|
||||
inherits `Generic ABS @System`) declares its own id, because its triple is its own.
|
||||
2. **The filament name is the base name**: the preset name with everything from the first
|
||||
(optionally space-preceded) `@` stripped. `MyBrand PLA @Orca 3D Fuse1` and `MyBrand PLA@HS`
|
||||
are both the filament `MyBrand PLA`.
|
||||
3. **Within one filament, variants' `compatible_printers` are pairwise disjoint** — per printer,
|
||||
at most one compatible instantiated preset per id, or AMS matching turns ambiguous. The
|
||||
C++ validator's `-f` check enforces this, tree-wide in CI. Since one product carries one id
|
||||
and cannot be split onto two, this rule is the *only* remedy for such an ambiguity: narrow
|
||||
the `compatible_printers`, or retire the preset that duplicates another.
|
||||
4. **Generics belong to OrcaFilamentLibrary.** A vendor tuning a generic material inherits
|
||||
`Generic X @System`, keeps the `Generic X` base name (that alias is what hides the library
|
||||
preset on your printers, and it is what makes its triple — and so its id — the library's)
|
||||
and sets a non-empty `compatible_printers` — e.g. `Generic PLA @Sovol SV08 MAX` inherits
|
||||
`Generic PLA @System` and lists three Sovol nozzles. Renaming such a preset makes it a
|
||||
different product by rule 5, so it then needs its own id.
|
||||
5. **Ids follow the product identity.** The id is a pure function of the product triple
|
||||
`(filament_vendor, filament_type, filament name)`, so correcting any of them re-mints the id
|
||||
**by design**, applied by `--generate` (preview with `--dry-run`, confine with `--vendor`) and
|
||||
gated by the `--update-snapshot` diff; the exact sequence is in the FAQ. Nothing forwards
|
||||
the old value, so anything outside the tree that stored it — a device tray, a calibration
|
||||
record, a saved project — falls back to matching by filament type until the user re-selects
|
||||
the filament. Re-mint deliberately, and only to fix a genuinely wrong identity.
|
||||
(`renamed_from` still gates preset-*name* compatibility, as before.)
|
||||
|
||||
## Minting — nobody invents ids
|
||||
|
||||
New ids are deterministic, computed exactly like the `setting_id` precedent
|
||||
(the `setting_id` half of `scripts/orca_id_tool.py`):
|
||||
|
||||
```text
|
||||
FILAMENT_ID_NAMESPACE = uuid5(setting-id NAMESPACE, "filament_id")
|
||||
= c4d3ff49-4c32-5534-a3e3-00894157ab97
|
||||
filament_id = "OF" + base62_6( uuid5(FILAMENT_ID_NAMESPACE,
|
||||
"filament_product/<filament_vendor>/<filament_type>/<filament_name>") )
|
||||
```
|
||||
|
||||
`base62_6` is the low 6 base62 digits (alphabet `0-9A-Za-z`) of the UUID taken as a big-endian
|
||||
integer, most-significant digit first; with the `OF` prefix the full id is 8 chars, within the
|
||||
AMS length limit. The triple comes from the root preset's *flattened* config:
|
||||
`<filament_vendor>` is the filament
|
||||
**manufacturer** (`"Polymaker"`, or `"Generic"` for generics — never the printer brand),
|
||||
`<filament_type>` the material type, `<filament_name>` the root's base name; the two config
|
||||
values are inheritable list options and the first element counts.
|
||||
|
||||
Content-addressing on that triple is what makes the whole system converge. The key contains no
|
||||
bundle name, so the same product mints the same id in every bundle — moving a filament into
|
||||
OrcaFilamentLibrary never changes its id, and two vendors independently shipping the same
|
||||
product arrive at the same id without coordinating. `Polymaker/PLA/PolyLite PLA` mints
|
||||
`OF5CgdDq`, and that one id is declared by the OrcaFilamentLibrary, OrcaArena, Qidi, and
|
||||
Snapmaker bundles alike; the OFL generic `Generic/PLA/Generic PLA` mints `OFDSrzZ8`, claimed
|
||||
by 35 bundles — most by independent declarations converging on the same mint, the rest
|
||||
purely through inheritance from the OFL preset.
|
||||
|
||||
Nothing but the triple feeds the mint — not the rest of the tree, not the snapshot, not what
|
||||
another preset of the product happens to carry. Determined triple, determined id: one product
|
||||
carries one id and there is no second acceptable value for it, so any other value on a preset
|
||||
is a mismatch `--check` reports and `--generate` pulls back. Two *different* products whose
|
||||
triples mint the same base62 value would be a collision (a roughly 36-bit id space against a
|
||||
few thousand products); nothing salts past it: `--check` reports it naming both products,
|
||||
`--generate` refuses to write it, and the remedy is a rename so their triples differ. Where
|
||||
two presets of one product would be AMS-ambiguous on a printer, the fix is likewise in the
|
||||
profiles — make their `compatible_printers` disjoint (structure rule 3), retire the redundant
|
||||
preset, or, if they really are different products, give them different names so their triples
|
||||
differ. Never a second id for one triple.
|
||||
|
||||
Workflow for a new filament:
|
||||
|
||||
```bash
|
||||
# 1. Author the filament with NO filament_id key anywhere.
|
||||
python scripts/orca_id_tool.py --dry-run # 2. preview the ids — writes nothing
|
||||
python scripts/orca_id_tool.py --generate # 3. apply them to the profile file(s)
|
||||
python scripts/orca_id_tool.py --update-snapshot # 4. record the new claims in the snapshot
|
||||
python scripts/orca_id_tool.py --check # 5. validate the filament_id state
|
||||
python scripts/orca_extra_profile_check.py # 6. ...and everything else CI checks
|
||||
# 7. Commit the profile edits together with scripts/filament_id_snapshot.json, for review.
|
||||
```
|
||||
|
||||
`--generate` makes every filament's id equal the mint of its own
|
||||
`(filament_vendor, filament_type, filament name)` triple: it inserts one where an instantiated
|
||||
filament resolves none, and re-derives one that does not match. A preset that *inherits* a
|
||||
mismatching id is the one case left to the author — check 3b names it, and the fix is to inherit
|
||||
a preset of the same filament or to give the preset its own key. A declaration is left alone
|
||||
exactly when it already equals the one id its triple mints, and a collision (check 3d) is
|
||||
reported and left unwritten. The same run assigns
|
||||
`generate_preset_setting_id(vendor, type, name)` to every instantiated filament, process
|
||||
and machine preset of every vendor except BBL, which keeps its authoritative `G*` ids, strips
|
||||
`setting_id` from base profiles, and fixes the misspelled `settings_id` key — dropped, or, for
|
||||
BBL, whose ids have no formula to fall back on, restored under the correct name. It is idempotent and
|
||||
byte-preserving (indentation, BOM, and line endings intact, every edited file re-parsed to fail
|
||||
loudly), and a no-op on a tree that already passes `scripts/orca_extra_profile_check.py` — the
|
||||
check CI runs over both id kinds, of which `--check` is the `filament_id` half.
|
||||
|
||||
- `--filament-id` limits the run to `filament_id`.
|
||||
- `--setting-id` limits the run to `setting_id`. The two exclude each other; pass neither to
|
||||
write both.
|
||||
- `--vendor VENDOR` confines the run to that bundle; repeatable. The id is a function of the
|
||||
triple alone, so a narrowed run writes exactly what a full one would; `--check` reports
|
||||
whatever it left outside.
|
||||
- `--dry-run` reports what `--generate` would do and writes nothing; with no mode of its own it
|
||||
implies `--generate`, so `--dry-run --vendor <Vendor>` previews just that bundle.
|
||||
- `--profiles DIR` points the tooling at a different profile tree (default
|
||||
`resources/profiles`). `--check` and `--update-snapshot` read and write the sanctioned state of
|
||||
the tree they are given, so pointing them elsewhere needs `--snapshot PATH` for that tree too —
|
||||
`scripts/filament_id_snapshot.json` describes `resources/profiles` and no other tree.
|
||||
|
||||
**Identity fixes need no separate mode.** `--generate` re-derives an id that no longer matches its
|
||||
triple exactly the way it fills in a missing one, so a rename or a `filament_vendor` /
|
||||
`filament_type` correction is just: fix the config, run `--generate` (confine it with `--vendor`,
|
||||
preview it with `--dry-run`), then `--update-snapshot` and review the diff.
|
||||
|
||||
If you skip the tooling, CI fails and prints the remedy: the expected id for your filament and
|
||||
the instruction to run `python scripts/orca_id_tool.py --generate`; once the id is minted, the
|
||||
snapshot checks likewise point at `--update-snapshot` and tell you to commit the resulting
|
||||
diff.
|
||||
|
||||
## Reserved namespaces — never mint or hand-write into
|
||||
|
||||
A **reserved namespace** is an id space no system profile may declare, because an external
|
||||
catalog or a device protocol owns the values. None of them has an owning vendor: there is no
|
||||
bundle — not even the one whose printers use the catalog — that may write one into a profile.
|
||||
|
||||
| Space | Status | Rule |
|
||||
| --- | --- | --- |
|
||||
| `GF*` | Bambu AMS/RFID catalog | declarable by **nobody**, BBL included: Bambu's own ids live in the generated catalog map, never in a profile |
|
||||
| `QD_*` | Qidi device protocol | declarable by **nobody**, Qidi included: the box composes these ids at runtime and they are not preset ids |
|
||||
| `P` + 7 hex chars (case-insensitive), `"null"` | user-created custom filaments (`CreatePresetsDialog.cpp`) | never appears in system profiles |
|
||||
|
||||
The two device namespaces, in detail:
|
||||
|
||||
- **Bambu (`GF*`).** Bambu's device/RFID/cloud catalog is external and opaque, which is a
|
||||
reason to keep it out of the profiles rather than to let one bundle own it. Every BBL filament
|
||||
mints an `OF` id from its triple like every other vendor's, and the correspondence to Bambu's
|
||||
catalog ids lives in one generated file the app applies at the printer boundary — the next
|
||||
section. Nothing under `resources/profiles/**` carries a `GF*` id today and nothing can be
|
||||
exempted, so a `GF*` id appearing anywhere in the tree is a mistake, whoever wrote it.
|
||||
- **Qidi (`QD_*`).** `QD_*` is a device-*protocol* namespace, not a preset id space: the
|
||||
Qidi box path composes `QD_<series>_<vendor>_<typeidx>` ids at runtime (slot vendor and
|
||||
type indices reported by the device, the series digit inferred client-side from the printer
|
||||
model/name). Qidi presets carry ordinary minted `OF*` ids (generics share the OFL ids), so
|
||||
a composed id matches no preset and the slot falls back to filament type; translating it to
|
||||
the filament's id belongs in `QidiPrinterAgent`. The alternative — treating per-series
|
||||
protocol ids as preset ids — would put one product under five ids (`QIDI PLA Rapido` would
|
||||
be `QD_0_1_1` through `QD_4_1_1`), exactly the fragmentation the mint rule removes.
|
||||
|
||||
## The Bambu catalog map
|
||||
|
||||
Bambu's printers, its AMS and its cloud know only Bambu's own catalog ids. Our profiles carry
|
||||
minted `OF` ids like every other vendor's, so one generated file records the correspondence and
|
||||
the app applies it **only where an id crosses to or from a Bambu printer**.
|
||||
|
||||
**The file** is `resources/printers/bambu_filament_ids.json` — a header plus one row per
|
||||
catalogued product, keyed by our id:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "https://github.com/bambulab/BambuStudio",
|
||||
"bambustudio_commit": "66e405477",
|
||||
"generated": "2026-09-04",
|
||||
"filaments": {
|
||||
"OFhuaUQB": { "bambu_id": "GFB00", "vendor": "Bambu Lab", "type": "ABS", "name": "Bambu ABS" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It ships in `resources/printers/`, next to `filaments_blacklist.json` — deliberately not in
|
||||
`resources/profiles/`, where the loader reads every top-level `.json` as a vendor index. It
|
||||
holds 100 rows today, one per product BambuStudio ships, and the correspondence is
|
||||
one-to-one in both directions.
|
||||
|
||||
**It is generated, never hand-edited.** `python scripts/update_bambu_filament_ids.py` rebuilds
|
||||
it from **BambuStudio's own shipped BBL bundle** — a sparse shallow clone of upstream `master`,
|
||||
or `--bambustudio-dir <a BambuStudio resources/profiles checkout>`. Our BBL bundle is a fork of
|
||||
Bambu's, tuned and extended independently, so it is not the source of truth for Bambu's ids.
|
||||
A row's key is the id the product's `(filament_vendor, filament_type, filament name)` triple
|
||||
mints — the same id any bundle of ours carries for it, since the id is a function of the triple
|
||||
alone; the row of a product we do not ship sits inert until some bundle claims that triple —
|
||||
`OFdyfQvU` / `GFG03`, "Bambu PETG Matte", is such a row today.
|
||||
|
||||
**Regenerate it in the same commit as every BBL profile sync**, and read the drift report it
|
||||
prints. Two lines, both informational, neither blocking the write:
|
||||
|
||||
```text
|
||||
upstream ships 'Bambu PETG Matte' (Bambu Lab/PETG), we ship nothing with that identity
|
||||
Orca BBL filaments with no row: 135 Orca-only product(s)
|
||||
```
|
||||
|
||||
The first names each upstream product our BBL bundle has no same-identity filament for —
|
||||
sometimes a genuinely missing product, sometimes a name drift a follow-up rename would
|
||||
converge. The second counts our own BBL filaments that matched no row: 135 of 234 today, of
|
||||
which 109 send an `OF` id on the wire and 26 already rode `OF` ids inherited from the
|
||||
OrcaFilamentLibrary. **135 is the number to expect at every regeneration** — 109 was the
|
||||
one-off size of the transition and stopped being computable from the tree once the BBL bundle
|
||||
was re-minted, so do not "fix" the report to print it.
|
||||
|
||||
**Check 6** lives in `check_filament_ids`, so profile CI runs it alongside the other five. It
|
||||
holds the file to its contract: it parses, carries `source` / `bambustudio_commit` /
|
||||
`generated`, keys only `OF`-format ids, maps each Bambu id at most once, and — for every row
|
||||
whose key the tree actually claims — agrees with the tree on that id's `(vendor, type, name)`
|
||||
triple. A row for a product we do not ship is skipped, not an error. The remedy it prints is
|
||||
always the same: regenerate the map and commit the diff for review.
|
||||
|
||||
### The runtime rule: swap on hit
|
||||
|
||||
Outbound, our id with a row becomes Bambu's; inbound, Bambu's id with a row becomes ours.
|
||||
Everything else is forwarded untouched — an `OF` id with no row, a Bambu id for a product we do
|
||||
not ship, a `P`-hex user id, `"null"`, an empty string. Translation is confined to the
|
||||
boundary: nothing between the boundaries ever holds a Bambu id.
|
||||
|
||||
Translating one value is a capability of the printer agent: `IPrinterAgent` declares
|
||||
`to_orca_filament_id` and `from_orca_filament_id` returning their argument, and `BBLPrinterAgent`
|
||||
overrides them with Bambu's map, so an agent whose printers already speak our ids inherits the
|
||||
identity default and translates nothing. `NetworkAgent` forwards both to the live agent, so the
|
||||
comparison sites below reach them through `wxGetApp().getAgent()` and leave an id untranslated
|
||||
while no agent is live. Whole documents are Bambu's business alone:
|
||||
`BBLPrinterAgent::to_orca_payload` and `from_orca_payload` rewrite every string under
|
||||
`tray_info_idx`, `filament_id` or `filamentId` at any depth; text that does not parse, or carries
|
||||
none of those keys, comes back unchanged. The map is loaded once, lazily; a missing or malformed
|
||||
file degrades to identity with a log line rather than failing.
|
||||
|
||||
| Boundary | Where it translates |
|
||||
| --- | --- |
|
||||
| Everything the agent sends | `BBLPrinterAgent::send_message` and `send_message_to_printer`, plus `PrintParams::ams_mapping_info` in `dispatch_start` — the funnel all five `start_*` calls share |
|
||||
| Everything the agent receives | `set_on_message_fn` and `set_on_local_message_fn` wrap their callback, so `MachineObject::parse_json` and everything downstream see our ids only |
|
||||
| 3mf export | `Plater::export_3mf` writes Bambu's ids into `slice_info.config`, gated on `preset_bundle.is_bbl_vendor()` — the printer reads that file and knows only its own catalog, and no other vendor's export is affected. The CLI has its own writer in `OrcaSlicer.cpp`; it does the same, gated on the `printer_model` prefix that already decides `Print::is_BBL_printer()` for that run |
|
||||
| Project ingest | `Plater::priv::load_files` reverse-maps the project's `filament_ids` before the bundle ingests them, so a project saved by an older Orca or by BambuStudio still resolves the same presets |
|
||||
| Prints from the printer's SD card | `SelectMachineDialog::update_print_required_data` reverse-maps each plate's slice-info ids as it adopts the plates, so the AMS mapping dialog pairs them with trays |
|
||||
| Bambu-specific comparisons | `CalibUtils.cpp`, `DeviceManager.cpp`, `DeviceCore/DevFilaSystem.cpp`, `DeviceCore/DevFilaBlackList.cpp`, `SelectMachine.cpp`, `AMSDryControl.cpp`, `AMSMaterialsSetting.cpp`, `PresetComboBoxes.cpp`, `ColorDecomposeSupport.cpp` |
|
||||
|
||||
That last row is the rule to follow when a new Bambu-specific behaviour is added: **translate
|
||||
the value you are about to compare, never the table you compare it against.** The shipped data
|
||||
those sites read is Bambu's and stays verbatim — `white_fila_ids` in
|
||||
`resources/printers/filaments_blacklist.json`, the calibration id lists in
|
||||
`resources/printers/<model>.json`, `fila_id` in
|
||||
`resources/profiles/BBL/filament/filaments_color_codes.json`.
|
||||
|
||||
`tests/slic3rutils/test_bambu_filament_ids.cpp` covers the lookups, the payload rewrite and the
|
||||
Bambu-specific rules. `orcaslicer_discover_tests` registers a Catch2 tag as a CTest **label**,
|
||||
not as part of the test name, so `-R` matches nothing here and the filter is `-L`:
|
||||
|
||||
```bash
|
||||
ctest --test-dir <build dir>/tests/slic3rutils -L BambuFilamentIds
|
||||
```
|
||||
|
||||
### Three places the map deliberately does not reach
|
||||
|
||||
The map and its lookups live in the GUI library, which libslic3r cannot link against and which a
|
||||
GUI-less build does not link at all. Three consequences are known and documented; none is worth
|
||||
pulling the map down into libslic3r for.
|
||||
|
||||
- **The support display type in `PrintConfig.cpp`.** `DynamicPrintConfig::get_filament_type`
|
||||
picks `PLA-S` / `Sup.PLA` and `PA-S` / `Sup.PA` for a support filament by testing
|
||||
`filament_id` against `GFS00` and `GFS01`, and otherwise falls back on `filament_type` — a
|
||||
fallback that returns those same two pairs for `"PLA"` and `"PA"`. Bambu Support W inherits
|
||||
`fdm_filament_pla` and Bambu Support G inherits `fdm_filament_pa`, so with their `OF` ids the
|
||||
fallback produces exactly what the id branches produced. (The only config that ever carries a
|
||||
singular `filament_id` key is the AMS tray config built in `Plater.cpp`, and that one never
|
||||
reaches this function.) These two lines are the only mention of a Bambu id anywhere in
|
||||
libslic3r, and they need no change.
|
||||
- **Config imports.** `PresetBundle::import_presets` (File ▸ Import ▸ Import Configs, for
|
||||
`.json` / `.zip` / `.orca_filament` / `.orca_printer` / `.orca_bundle`) and
|
||||
`PresetBundle::load_config_file` (the CLI's `--load-settings` of a G-code file with an
|
||||
embedded config) both parse inside libslic3r, out of the GUI's reach, so a Bambu id carried
|
||||
in such a file lands on the imported preset untranslated. The effect is bounded: that preset
|
||||
does not auto-match an AMS tray while the stale id is live, and the id does not survive
|
||||
being saved — `Preset::save` writes a `filament_id` key only for a preset whose `inherits` is
|
||||
empty, and on the next load an inheriting preset takes its parent's id. A known gap, and not
|
||||
a regression: nothing forwarded a stale id before either.
|
||||
- **A build configured without the GUI.** `target_link_libraries(OrcaSlicer libslic3r_gui)` sits
|
||||
inside `if (SLIC3R_GUI)` in `src/CMakeLists.txt`, so the lookups are not linkable when the GUI
|
||||
is off. The CLI's 3mf writer in `src/OrcaSlicer.cpp` therefore guards its translation with
|
||||
`#ifdef SLIC3R_GUI`, and a 3mf that such a build slices for a Bambu printer carries our `OF`
|
||||
ids in `slice_info.config` rather than Bambu's. Every shipped build enables the GUI, so this
|
||||
reaches only a purpose-built GUI-less binary.
|
||||
|
||||
One more thing worth recording before it is rediscovered:
|
||||
`SyncAmsInfoDialog::update_print_required_data` is a structural twin of the SD-card function
|
||||
above and carries no translation. It has no callers today and its plate list is only ever read
|
||||
for `printer_model_id`, so it is not a live gap — but wiring it up without adding the reverse
|
||||
map would silently reproduce the bug.
|
||||
|
||||
## How CI enforces this
|
||||
|
||||
Profile CI (`check_profiles.yml`) runs `check_filament_ids()` tree-wide via
|
||||
`scripts/orca_extra_profile_check.py`. Its ground truth is
|
||||
**`scripts/filament_id_snapshot.json` — the sanctioned state**: the id state derived from the
|
||||
tree must equal the snapshot exactly, in both directions. Any change to the id landscape
|
||||
therefore surfaces as a diff to that file, and **that snapshot diff is what maintainers review
|
||||
and gate in a PR**. Never edit the snapshot by hand — `--update-snapshot` regenerates it
|
||||
deterministically (running it twice changes nothing). The snapshot holds one map, `ids`: each
|
||||
entry is the product the id is minted from (`filament_vendor`, `filament_type`, `name`) and the
|
||||
`filaments` claiming it (`Vendor/Filament`), and it sanctions *state*, never exceptions: no check
|
||||
consults it to excuse a preset from a rule, and there is no grandfather list of any kind.
|
||||
|
||||
The checks, in brief:
|
||||
|
||||
- **Format** — every id occurring in the tree is `OF` + 6 base62 chars. No exceptions: not a
|
||||
snapshot entry, not BBL.
|
||||
- **Snapshot equality** — tree claims == snapshot claims **and** each id's declared triple ==
|
||||
its snapshot entry, both directions: any `filament_vendor`/`filament_type`/name change
|
||||
surfaces as a snapshot diff.
|
||||
- **Identity** — the id is a function of the triple alone. A declared `OF*` id must equal the
|
||||
one id its declarer's own triple mints, with no second acceptable value; the id an
|
||||
instantiated preset *inherits* must equal the mint of *its* own triple, however it inherits
|
||||
it (a root, a real filament, a library preset — structure rule 1); and every instantiated
|
||||
system filament must resolve an effective id at all (recall: an id-less one is a hard load
|
||||
error in C++ that discards the whole vendor bundle); and no two products mint one id (a
|
||||
base62 collision, resolved by renaming one of them). The errors print the expected id.
|
||||
- **Reserved namespaces** — `GF*`, `QD_*`, `P<7-hex>` or `"null"` claimed by any vendor,
|
||||
BBL and Qidi included.
|
||||
- **Triple integrity** — every declarer must resolve a non-empty `filament_vendor` and
|
||||
`filament_type` (generics use `"Generic"`), and all declarers of one filament within a
|
||||
bundle must agree on the triple.
|
||||
- **Bambu catalog map** — `resources/printers/bambu_filament_ids.json` parses, carries its
|
||||
`source` / `bambustudio_commit` / `generated` header, keys only `OF`-format ids, maps each
|
||||
Bambu id at most once, and agrees with the tree on the triple of every row whose key the
|
||||
tree claims. See [The Bambu catalog map](#the-bambu-catalog-map); the remedy is always to
|
||||
regenerate, never to hand-edit.
|
||||
|
||||
A profile that declares a **reserved-namespace** id — `GF*`, `QD_*` or `P<7-hex>`, whatever
|
||||
its vendor — cannot pass the format check, so `--update-snapshot` refuses to sanction it
|
||||
rather than hide the mistake until CI. For a Bambu-cataloged product, the catalog map is where
|
||||
the correspondence belongs. Any other new sharing via a *declared* id is caught by the identity
|
||||
check; sharing through inheritance carries no declaration to check and surfaces only as a new
|
||||
claim in the snapshot diff — which is exactly why that diff is the gate.
|
||||
|
||||
`orca_extra_profile_check.py` separately holds every declared id to the AMS 8-character limit,
|
||||
tree-wide and for every vendor alike, scoped to the presets a vendor's index actually
|
||||
references (a file the index never loads cannot break AMS matching).
|
||||
|
||||
Complementing the Python checks, CI also runs the C++ profile validator with `-f`
|
||||
(`check_filament_subtypes`): it loads the bundle exactly as the app does and flags any printer
|
||||
for which two or more compatible filament presets share one `filament_id` — the runtime-shaped
|
||||
ambiguity check behind structure rule 3.
|
||||
|
||||
## FAQ
|
||||
|
||||
- **A new color of an existing product?** Never a new id — colors are not filaments.
|
||||
- **A second diameter (1.75 mm and 2.85 mm) of the same product?** A sibling filament with its
|
||||
own id: two diameters are separately selectable spool products.
|
||||
- **A high-speed tune of an existing material for another printer model?** Same filament:
|
||||
keep the base name and inherit its root; no id key needed.
|
||||
- **A tuned generic ("our profile for Generic PLA")?** Inherit `Generic PLA @System`, keep the
|
||||
`Generic PLA` base name, set `compatible_printers`; no id key needed.
|
||||
- **A branded filament that borrows a generic's settings?** Fine — inherit `Generic X @System`
|
||||
(or any real filament) for the settings and declare the id of your own filament; run
|
||||
`python scripts/orca_id_tool.py --generate` to mint it. Inheritance never changes the id.
|
||||
- **I need to fix a filament's `filament_vendor` or `filament_type`.** Fix the config, run
|
||||
`--generate --vendor <Vendor>` (preview with `--dry-run`), then `--update-snapshot`, and commit
|
||||
the profile and snapshot diffs together. The id re-derives from the corrected identity, and
|
||||
nothing forwards the old value, so a tray or record still holding it falls back to matching by
|
||||
filament type.
|
||||
- **I need to rename a filament.** Rename the presets (adding `renamed_from`, which keeps the
|
||||
preset *name* resolving), then `--generate --vendor <Vendor>` (preview with `--dry-run`), then
|
||||
`--update-snapshot`. The id follows the new filament name; as with any identity fix, the old id
|
||||
is not forwarded.
|
||||
- **Can I reuse a `QD_*` id for a Qidi profile?** No — nobody can. It is the device protocol's
|
||||
own id space: the box composes those values at runtime and no preset carries one. Author
|
||||
Qidi filaments like any other vendor's.
|
||||
- **CI says my filament needs an id.** Run `python scripts/orca_id_tool.py --generate`, then
|
||||
`--update-snapshot`, and commit both diffs. Do not type an id by hand.
|
||||
|
||||
For general profile authoring, see the profile development guide on the
|
||||
[OrcaSlicer wiki](https://www.orcaslicer.com/wiki).
|
||||
@@ -0,0 +1,376 @@
|
||||
# macOS FFmpeg Media Player Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make macOS use the same FFmpeg-based media player (`wxMediaCtrl3` + `AVVideoDecoder`) as Windows/Linux, linking the static FFmpeg libraries from the deps build, and remove the old `wxMediaCtrl2.mm` BambuPlayer-based player.
|
||||
|
||||
**Architecture:** The new player is platform-neutral C++ already used on Linux/Windows. Enabling it on macOS is pure build wiring: compile `wxMediaCtrl3.cpp` + `AVVideoDecoder.cpp` on macOS, drop the `__WXMAC__` alias that redirects `wxMediaCtrl3` to the old `wxMediaCtrl2`, and link static FFmpeg (`libavcodec.a`/`libswscale.a`/`libavutil.a`) from the deps install. The Bambu stream API is dlsym'd at runtime from the network plugin (`libBambuSource.dylib`), which already exports it — no plugin changes needed. Rendering reuses the existing `wxImage` → `DrawBitmap` paint path (same as Linux).
|
||||
|
||||
**Tech Stack:** C++17, wxWidgets, CMake, FFmpeg 7.0.3 (libavcodec/libswscale/libavutil), macOS (Xcode generator), `deps/` ExternalProject build system.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Branch: `dev/ffmpeg-player-macos`. Commit after every task.
|
||||
- **Linux and Windows builds must not change** — the FFmpeg deps flag change is guarded by `APPLE`; Linux keeps `--enable-shared`, Windows keeps its prebuilt DLL zips.
|
||||
- Static FFmpeg only on macOS: deps produce `libavcodec.a`/`libswscale.a`/`libavutil.a`; the app links those explicitly — the app binary must have **no** `libav*` dylib references (`otool -L` check).
|
||||
- Follow existing code style: PascalCase classes, snake_case functions, C++17.
|
||||
- No changes to `StatusPanel.cpp`, `MediaPlayCtrl.*`, or the BambuTunnel interface — the app already creates `wxMediaCtrl3` and uses only its public interface.
|
||||
- The player cannot be unit-tested (hardware/plugin-dependent GUI code); verification is build-level, link-level, and manual runtime on a Mac.
|
||||
- `localization/i18n/list.txt` references only `wxMediaCtrl2.cpp` (Win/Linux, stays) — no translation-list changes needed.
|
||||
- Build dirs on the dev machine: main app = `build_arm64/` (Xcode generator, multi-config), deps = `deps/build/arm64/` (Unix Makefiles). App target name: `OrcaSlicer`. Substitute your own configured build dirs where noted.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Enable wxMediaCtrl3 on macOS and link static FFmpeg
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/slic3r/GUI/wxMediaCtrl3.h` (lines 18–22: the `#ifdef __WXMAC__` alias branch)
|
||||
- Modify: `src/slic3r/GUI/wxMediaCtrl3.cpp:13` (uncomment the event define)
|
||||
- Modify: `src/slic3r/GUI/wxMediaCtrl2.cpp:101` (remove the event define)
|
||||
- Modify: `src/slic3r/CMakeLists.txt` (APPLE source list ~lines 779–792; FFmpeg link block ~lines 905–910)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nothing new (all classes already exist).
|
||||
- Produces: `wxMediaCtrl3` class compiled on macOS with the same interface as Linux/Windows — `Load(wxURI)`, `Play()`, `Stop()`, `SetIdleImage(wxString)`, `GetState()`, `GetLastError()`, `GetVideoSize()`, event `EVT_MEDIA_CTRL_STAT` defined once in the lib (from `wxMediaCtrl3.cpp`).
|
||||
|
||||
- [ ] **Step 1: Remove the macOS alias in wxMediaCtrl3.h**
|
||||
|
||||
Current (lines 16–23 of `src/slic3r/GUI/wxMediaCtrl3.h`):
|
||||
|
||||
```cpp
|
||||
void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height);
|
||||
|
||||
#ifdef __WXMAC__
|
||||
|
||||
#include "wxMediaCtrl2.h"
|
||||
#define wxMediaCtrl3 wxMediaCtrl2
|
||||
|
||||
#else
|
||||
|
||||
#define BAMBU_DYNAMIC
|
||||
```
|
||||
|
||||
New:
|
||||
|
||||
```cpp
|
||||
void wxMediaCtrl_OnSize(wxWindow * ctrl, wxSize const & videoSize, int width, int height);
|
||||
|
||||
#define BAMBU_DYNAMIC
|
||||
```
|
||||
|
||||
Also remove the matching `#endif` that closed the `#else` branch (the one before the final `#endif /* wxMediaCtrl3_h */`), so the file's `#ifndef`/`#endif` guard pair stays balanced.
|
||||
|
||||
- [ ] **Step 2: Move the EVT_MEDIA_CTRL_STAT definition into wxMediaCtrl3.cpp**
|
||||
|
||||
In `src/slic3r/GUI/wxMediaCtrl3.cpp:13`, uncomment:
|
||||
|
||||
```cpp
|
||||
//wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
|
||||
```
|
||||
|
||||
becomes:
|
||||
|
||||
```cpp
|
||||
wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
|
||||
```
|
||||
|
||||
In `src/slic3r/GUI/wxMediaCtrl2.cpp:101`, delete:
|
||||
|
||||
```cpp
|
||||
wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent);
|
||||
```
|
||||
|
||||
(One definition total in the lib — `MediaPlayCtrl.cpp:59` binds this event on the media ctrl.)
|
||||
|
||||
- [ ] **Step 3: Update the APPLE source list in CMakeLists.txt**
|
||||
|
||||
In `src/slic3r/CMakeLists.txt`, the APPLE branch (currently compiles `wxMediaCtrl2.mm`, which becomes dead on macOS):
|
||||
|
||||
```cmake
|
||||
GUI/wxMediaCtrl2.mm
|
||||
GUI/wxMediaCtrl2.h
|
||||
GUI/wxMediaCtrl3.h
|
||||
)
|
||||
```
|
||||
|
||||
becomes:
|
||||
|
||||
```cmake
|
||||
GUI/AVVideoDecoder.cpp
|
||||
GUI/AVVideoDecoder.hpp
|
||||
GUI/wxMediaCtrl3.cpp
|
||||
GUI/wxMediaCtrl3.h
|
||||
)
|
||||
```
|
||||
|
||||
(The `else ()` branch — Win/Linux — stays exactly as it is.)
|
||||
|
||||
- [ ] **Step 4: Link static FFmpeg on macOS**
|
||||
|
||||
In `src/slic3r/CMakeLists.txt`, the FFmpeg block (currently `if (NOT APPLE)`):
|
||||
|
||||
```cmake
|
||||
if (NOT APPLE)
|
||||
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
|
||||
libavcodec
|
||||
libswscale
|
||||
libavutil
|
||||
)
|
||||
target_link_libraries(libslic3r_gui PkgConfig::LIBAV)
|
||||
endif()
|
||||
```
|
||||
|
||||
becomes:
|
||||
|
||||
```cmake
|
||||
if (APPLE)
|
||||
# Static FFmpeg from the deps install: nothing to bundle into the .app,
|
||||
# no rpath/install_name handling. Order matters: avcodec -> swscale -> avutil.
|
||||
find_library(LIBAVCODEC_LIBRARY NAMES libavcodec.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
|
||||
find_library(LIBSWSCALE_LIBRARY NAMES libswscale.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
|
||||
find_library(LIBAVUTIL_LIBRARY NAMES libavutil.a PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
|
||||
target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY})
|
||||
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include)
|
||||
else ()
|
||||
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
|
||||
libavcodec
|
||||
libswscale
|
||||
libavutil
|
||||
)
|
||||
target_link_libraries(libslic3r_gui PkgConfig::LIBAV)
|
||||
endif()
|
||||
```
|
||||
|
||||
The deps install (`${CMAKE_PREFIX_PATH}/lib`) already contains the three `.a` files from the existing arm64 deps build — no deps rebuild needed for this task.
|
||||
|
||||
- [ ] **Step 5: Reconfigure and build the app**
|
||||
|
||||
Run (Xcode generator; `cmake` re-runs automatically on build):
|
||||
|
||||
```bash
|
||||
cmake --build build_arm64 --config RelWithDebInfo --target OrcaSlicer
|
||||
```
|
||||
|
||||
Expected: configure succeeds (no `pkg_check_modules` errors on macOS, `find_library` finds all three `.a` files), compile succeeds (`wxMediaCtrl3.cpp` and `AVVideoDecoder.cpp` compile on macOS without changes), link succeeds.
|
||||
|
||||
If CMake complains that `wxMediaCtrl3.h` is included but not in the source list or similar IDE-only warnings — ignore; headers in the list are cosmetic.
|
||||
|
||||
- [ ] **Step 6: Verify no dynamic FFmpeg dependency**
|
||||
|
||||
```bash
|
||||
otool -L build_arm64/src/RelWithDebInfo/OrcaSlicer.app/Contents/MacOS/OrcaSlicer | grep -i "libav" || echo "OK: no dynamic FFmpeg"
|
||||
```
|
||||
|
||||
Expected: prints `OK: no dynamic FFmpeg` (empty grep output). This is the whole point of static linking — nothing to bundle into the `.app`.
|
||||
|
||||
- [ ] **Step 7: Quick sanity — macOS unit tests still pass**
|
||||
|
||||
```bash
|
||||
ctest --test-dir build_arm64/tests/libslic3r --output-on-failure
|
||||
```
|
||||
|
||||
Expected: passes (add `-C RelWithDebInfo` if the multi-config generator requires it). If no tests were built in this build dir, build target `tests` first (`cmake --build build_arm64 --config RelWithDebInfo --target tests`).
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add src/slic3r/CMakeLists.txt src/slic3r/GUI/wxMediaCtrl3.h src/slic3r/GUI/wxMediaCtrl3.cpp src/slic3r/GUI/wxMediaCtrl2.cpp
|
||||
git commit -m "feat: use FFmpeg media player on macOS with static FFmpeg"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Static-only FFmpeg in the macOS deps build
|
||||
|
||||
**Files:**
|
||||
- Modify: `deps/FFMPEG/FFMPEG.cmake` (non-MSVC branch, APPLE section and CONFIGURE_COMMAND)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nothing.
|
||||
- Produces: a deps install on macOS containing only `libavcodec.a`, `libswscale.a`, `libavutil.a` (+ headers) — no `libav*` dylibs, so no bundling/rpath machinery is ever needed on macOS. Linux and Windows output are unchanged.
|
||||
|
||||
- [ ] **Step 1: Add the static flag variable**
|
||||
|
||||
In `deps/FFMPEG/FFMPEG.cmake`, inside the non-MSVC `else ()` branch, in the existing `if (APPLE)` block:
|
||||
|
||||
```cmake
|
||||
if (APPLE)
|
||||
set(_minos_cmd
|
||||
"CFLAGS=-mmacosx-version-min=${DEP_OSX_TARGET}"
|
||||
"LDFLAGS=-mmacosx-version-min=${DEP_OSX_TARGET}"
|
||||
)
|
||||
```
|
||||
|
||||
add after the `_minos_cmd` set:
|
||||
|
||||
```cmake
|
||||
# Static FFmpeg: nothing to bundle into the .app, no rpath handling.
|
||||
# Shared flags must come AFTER --enable-shared below so they win.
|
||||
set(_link_cmd --enable-static --disable-shared)
|
||||
```
|
||||
|
||||
and add a matching `else ()` after the `if (IS_CROSS_COMPILE) ... endif()` block inside that `if (APPLE)`, so non-Apple Unix keeps shared:
|
||||
|
||||
```cmake
|
||||
else ()
|
||||
set(_link_cmd --enable-shared)
|
||||
endif ()
|
||||
```
|
||||
|
||||
(If the existing `if (IS_CROSS_COMPILE)` block is the last thing inside `if (APPLE)`, the new `else ()` closes the `if (APPLE)` itself.)
|
||||
|
||||
- [ ] **Step 2: Use the variable in CONFIGURE_COMMAND**
|
||||
|
||||
In the `ExternalProject_Add(dep_FFMPEG ...)` configure command:
|
||||
|
||||
```cmake
|
||||
"--prefix=${DESTDIR}"
|
||||
--enable-shared
|
||||
```
|
||||
|
||||
becomes:
|
||||
|
||||
```cmake
|
||||
"--prefix=${DESTDIR}"
|
||||
--enable-shared
|
||||
${_link_cmd}
|
||||
```
|
||||
|
||||
Order matters: `--enable-shared` comes first, then `--enable-static --disable-shared` (APPLE) or `--enable-shared` (Linux) — the last flag wins in FFmpeg configure.
|
||||
|
||||
- [ ] **Step 3: Rebuild the FFmpeg dep (slow — several minutes, run in background)**
|
||||
|
||||
The changed CONFIGURE_COMMAND invalidates the ExternalProject stamp, so this re-configures and rebuilds FFmpeg:
|
||||
|
||||
```bash
|
||||
cmake --build deps/build/arm64 --target dep_FFMPEG
|
||||
```
|
||||
|
||||
For a fully clean static-only check (removes the previous shared build tree, which can leave stale `.dylib` files behind in the in-source build):
|
||||
|
||||
```bash
|
||||
rm -rf deps/build/arm64/dep_FFMPEG-prefix
|
||||
cmake --build deps/build/arm64 --target dep_FFMPEG
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify the artifacts**
|
||||
|
||||
```bash
|
||||
ls deps/build/arm64/dep_FFMPEG-prefix/src/dep_FFMPEG/libavcodec/*.a
|
||||
ls deps/build/arm64/dep_FFMPEG-prefix/src/dep_FFMPEG/libavcodec/*.dylib 2>/dev/null || echo "OK: no dylibs"
|
||||
```
|
||||
|
||||
Expected: `libavcodec.a` present, second command prints `OK: no dylibs`. Check `libavutil` and `libswscale` the same way.
|
||||
|
||||
- [ ] **Step 5: Verify the app still links against the static libs**
|
||||
|
||||
```bash
|
||||
cmake --build build_arm64 --config RelWithDebInfo --target OrcaSlicer
|
||||
otool -L build_arm64/src/RelWithDebInfo/OrcaSlicer.app/Contents/MacOS/OrcaSlicer | grep -i "libav" || echo "OK: no dynamic FFmpeg"
|
||||
```
|
||||
|
||||
Expected: build succeeds, `OK: no dynamic FFmpeg`.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add deps/FFMPEG/FFMPEG.cmake
|
||||
git commit -m "build: build static-only FFmpeg for macOS deps"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Remove the old macOS player
|
||||
|
||||
**Files:**
|
||||
- Delete: `src/slic3r/GUI/wxMediaCtrl2.mm`
|
||||
- Delete: `src/slic3r/GUI/BambuPlayer/BambuPlayer.h` (and the empty `BambuPlayer/` dir)
|
||||
- Modify: `src/slic3r/GUI/wxMediaCtrl2.h` (remove the `#ifdef __WXMAC__` section, lines 22–60)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 (macOS no longer references `wxMediaCtrl2` — nothing includes `wxMediaCtrl2.h` on macOS anymore; `wxMediaCtrl2` is never instantiated on any platform).
|
||||
- Produces: a clean tree where the old BambuPlayer-based player is gone from macOS. The `BambuPlayer` ObjC class itself remains inside the network plugin (external prebuilt binary) — only the GUI-side consumer is removed.
|
||||
|
||||
- [ ] **Step 1: Delete the old player files**
|
||||
|
||||
```bash
|
||||
git rm src/slic3r/GUI/wxMediaCtrl2.mm
|
||||
git rm src/slic3r/GUI/BambuPlayer/BambuPlayer.h
|
||||
rmdir src/slic3r/GUI/BambuPlayer 2>/dev/null || true
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Strip the __WXMAC__ section from wxMediaCtrl2.h**
|
||||
|
||||
In `src/slic3r/GUI/wxMediaCtrl2.h`, remove the entire macOS branch of the `#ifdef __WXMAC__` guard — from `#ifdef __WXMAC__` (line 22) through the closing `};` of the mac class (line 60), and the `#else` marker — leaving only the non-mac `class wxMediaCtrl2 : public wxMediaCtrl { ... };` definition followed by the final `#endif /* wxMediaCtrl2_h */`. The resulting file keeps its `#ifndef`/`#endif` include guard pair balanced.
|
||||
|
||||
The file stays on disk because Win/Linux compile `wxMediaCtrl2.cpp`, which includes it.
|
||||
|
||||
- [ ] **Step 3: Grep for leftover references**
|
||||
|
||||
```bash
|
||||
grep -rn "wxMediaCtrl2.mm\|BambuPlayer/BambuPlayer.h\|BambuPlayer" src/slic3r --include="*.cpp" --include="*.h" --include="*.mm" --include="*.txt"
|
||||
```
|
||||
|
||||
Expected: no hits in `src/slic3r/GUI` (ignore `localization/i18n/list.txt:196`, which lists the Win/Linux `wxMediaCtrl2.cpp` and stays).
|
||||
|
||||
- [ ] **Step 4: Rebuild the app**
|
||||
|
||||
```bash
|
||||
cmake --build build_arm64 --config RelWithDebInfo --target OrcaSlicer
|
||||
```
|
||||
|
||||
Expected: configure + compile + link succeed with the deleted files gone.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add -A src/slic3r/GUI
|
||||
git commit -m "refactor: remove old BambuPlayer-based media player from macOS"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Runtime verification on hardware
|
||||
|
||||
**Files:** none — manual verification.
|
||||
|
||||
**Interfaces:** consumes all prior tasks. Final gate: the new player must actually stream on a Mac.
|
||||
|
||||
- [ ] **Step 1: Launch the freshly built app**
|
||||
|
||||
```bash
|
||||
open build_arm64/src/RelWithDebInfo/OrcaSlicer.app
|
||||
```
|
||||
|
||||
Expected: app launches normally; no crash in the network/device subsystem.
|
||||
|
||||
- [ ] **Step 2: Load the network plugin and open the Device tab**
|
||||
|
||||
Log in / ensure the network plugin (`libBambuSource.dylib`) loads, select a printer, open the Device tab (camera monitoring panel).
|
||||
|
||||
Expected: the camera preview area shows the idle image initially (no crash — this exercises `wxMediaCtrl3::SetIdleImage` and the `wxImage` load path on macOS for the first time).
|
||||
|
||||
- [ ] **Step 3: Start the stream and watch it render**
|
||||
|
||||
Click play / wait for `MediaPlayCtrl` to start the stream.
|
||||
|
||||
Expected: live video renders in the panel. Check the console/log output (`BOOST_LOG` goes to the terminal if run from it, or check the log file):
|
||||
- `stat_log ...` lines appear (the `EVT_MEDIA_CTRL_STAT` path is live — proves the Bambu C API dlsym worked from `libBambuSource.dylib`);
|
||||
- no repeated decode/error messages like `AVVideoDecoder: ...` or `can not find function ...` (proves `StaticBambuLib::get` resolved all Bambu functions);
|
||||
- Stop/Play toggle works; idle image reappears on stop;
|
||||
- window resize keeps aspect ratio (exercises `DoSetSize`/`adjust_frame_size`/`paintEvent`).
|
||||
|
||||
- [ ] **Step 4: Confirm the old player is really gone**
|
||||
|
||||
Expected: nothing in the logs references `BambuPlayer` (the ObjC class is no longer dlsym'd); the video path is entirely `wxMediaCtrl3` + `AVVideoDecoder`.
|
||||
|
||||
If a printer is unavailable, at minimum verify Steps 1–2 (launch + idle image) and note in the PR that live-stream verification needs hardware.
|
||||
|
||||
- [ ] **Step 5: Final review pass**
|
||||
|
||||
```bash
|
||||
git log --oneline -6
|
||||
git show --stat HEAD # and each of the three task commits
|
||||
```
|
||||
|
||||
Expected: the last 4 commits are the design doc + the 3 implementation tasks (each task commit touches only its listed files). Review the diff for scope: no Linux/Windows changes beyond the two `EVT_MEDIA_CTRL_STAT` lines in Task 1, no `StatusPanel`/`MediaPlayCtrl` changes.
|
||||
@@ -0,0 +1,110 @@
|
||||
# FFmpeg Media Player for macOS — Design
|
||||
|
||||
Date: 2026-08-14
|
||||
Branch: `dev/ffmpeg-player-macos`
|
||||
|
||||
## Problem
|
||||
|
||||
The branch's new FFmpeg-based media player (`wxMediaCtrl3` + `AVVideoDecoder`) is used on
|
||||
Windows and Linux, but macOS still runs the old player: `wxMediaCtrl2.mm`, an ObjC
|
||||
`BambuPlayer` class dlsym'd from the Bambu network plugin that renders via CALayer.
|
||||
On macOS, `wxMediaCtrl3` is currently aliased to `wxMediaCtrl2` and FFmpeg is not linked
|
||||
into the app at all.
|
||||
|
||||
Goal: make macOS use the same FFmpeg player as Windows/Linux, linking the **static**
|
||||
FFmpeg libraries from the deps build instead of dynamic ones.
|
||||
|
||||
## Current state (verified)
|
||||
|
||||
- New player (Win/Linux): `GUI/wxMediaCtrl3.cpp` + `GUI/AVVideoDecoder.cpp`. Decodes with
|
||||
FFmpeg (libavcodec/libswscale/libavutil), renders frames into `wxImage` (non-Windows) /
|
||||
`wxBitmap` (Windows) drawn in a `paintEvent`, feeds via the `Bambu_*` C API
|
||||
(`BambuTunnel.h`, `BAMBU_DYNAMIC`) dlsym'd from the network plugin through
|
||||
`StaticBambuLib::get()` (`GUI/Printer/PrinterFileSystem.cpp`, compiled on all platforms).
|
||||
- Old player (macOS): `GUI/wxMediaCtrl2.mm` uses the ObjC `BambuPlayer` class found via
|
||||
`dlsym(module, "OBJC_CLASS_$_BambuPlayer")` in `libBambuSource.dylib`.
|
||||
- The macOS network plugin `libBambuSource.dylib` already exports the full Bambu C API
|
||||
(verified with `nm`), so the new player needs zero plugin changes.
|
||||
- FFmpeg linking in `src/slic3r/CMakeLists.txt` is guarded by `if (NOT APPLE)` —
|
||||
macOS currently does not link FFmpeg.
|
||||
- `deps/FFMPEG/FFMPEG.cmake`: non-MSVC branch builds FFmpeg from source with
|
||||
`--enable-shared`. The existing arm64 deps build on the dev machine happened to be
|
||||
configured with both static and shared enabled, so `libavcodec.a` / `libswscale.a` /
|
||||
`libavutil.a` are already present at
|
||||
`deps/build/arm64/OrcaSlicer_dep/usr/local/lib/`.
|
||||
- `EVT_MEDIA_CTRL_STAT` is `wxDEFINE_EVENT`'d in `wxMediaCtrl2.cpp` (Win/Linux) and
|
||||
`wxMediaCtrl2.mm` (macOS); the define in `wxMediaCtrl3.cpp` is commented out.
|
||||
- `wxMediaCtrl2` is never instantiated anywhere on any platform — dead code.
|
||||
- `StatusPanel` already creates `wxMediaCtrl3`; `MediaPlayCtrl` only uses the
|
||||
`wxMediaCtrl3` interface (`Load/Play/Stop/GetState/GetVideoSize/GetLastError/SetIdleImage`),
|
||||
so no UI-side changes are needed.
|
||||
|
||||
## Approach (approved)
|
||||
|
||||
**Reuse the shared player on macOS.** Compile the existing `wxMediaCtrl3.cpp` +
|
||||
`AVVideoDecoder.cpp` on macOS so all three platforms run one implementation.
|
||||
Rendering uses the existing `wxImage` → `DrawBitmap` paint path, identical to Linux.
|
||||
Known trade-off: frames are scaled to the widget's logical (1x) size, so Retina is
|
||||
slightly soft compared to the old CALayer player. Accepted for now; a Retina-aware
|
||||
scaling follow-up is possible later.
|
||||
|
||||
Rejected alternative: a native CGImage/CALayer renderer for macOS — faster and
|
||||
Retina-crisp, but adds a second render implementation to maintain.
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. Enable the FFmpeg player on macOS (source)
|
||||
|
||||
- `GUI/wxMediaCtrl3.h`: remove the `#ifdef __WXMAC__` branch (lines 18–22) that aliases
|
||||
`wxMediaCtrl3` → `wxMediaCtrl2`. macOS then compiles the real `wxMediaCtrl3` class,
|
||||
including the `BAMBU_DYNAMIC` BambuTunnel path used on Linux.
|
||||
- Event symbol fix: move `wxDEFINE_EVENT(EVT_MEDIA_CTRL_STAT, wxCommandEvent)` into
|
||||
`wxMediaCtrl3.cpp` (uncomment the existing line) and remove it from
|
||||
`wxMediaCtrl2.cpp`. One definition total in the lib; all three platforms resolve it.
|
||||
|
||||
### 2. Static FFmpeg linking (deps + app)
|
||||
|
||||
- `deps/FFMPEG/FFMPEG.cmake`: in the non-MSVC branch, pass
|
||||
`--disable-shared --enable-static` when `APPLE`. Linux keeps `--enable-shared`;
|
||||
Windows keeps its prebuilt shared DLL zips. Fresh macOS deps builds install only
|
||||
`libavcodec.a` / `libswscale.a` / `libavutil.a` — no dylibs to bundle, no
|
||||
rpath/install_name handling. (The existing local arm64 deps build already contains
|
||||
the `.a` files, so no deps rebuild is strictly needed to try the change locally,
|
||||
but a fresh CI deps build must produce them.)
|
||||
- `src/slic3r/CMakeLists.txt`:
|
||||
- APPLE branch of `SLIC3R_GUI_SOURCES`: add `GUI/wxMediaCtrl3.cpp`,
|
||||
`GUI/wxMediaCtrl3.h`, `GUI/AVVideoDecoder.cpp`, `GUI/AVVideoDecoder.hpp`;
|
||||
remove `GUI/wxMediaCtrl2.mm` and `GUI/wxMediaCtrl2.h` (the `.h` stays on
|
||||
disk for the Win/Linux build of `wxMediaCtrl2.cpp`, but nothing on macOS
|
||||
includes it after this change).
|
||||
- Add an APPLE mirror of the `NOT APPLE` FFmpeg block: `find_library` for
|
||||
`libavcodec.a`, `libswscale.a`, `libavutil.a` under `${CMAKE_PREFIX_PATH}/lib`
|
||||
with `NO_DEFAULT_PATH`, link them (order avcodec → swscale → avutil), and add
|
||||
`${CMAKE_PREFIX_PATH}/include` as a SYSTEM include directory. Deps are built with
|
||||
`--disable-zlib` and no external codecs, so the three static libs link cleanly.
|
||||
|
||||
### 3. Remove the old player
|
||||
|
||||
- Delete `GUI/wxMediaCtrl2.mm` and `GUI/BambuPlayer/BambuPlayer.h` (header used only
|
||||
by the `.mm`; the real `BambuPlayer` lives inside the network plugin).
|
||||
- Remove the now-dead `__WXMAC__` section of `GUI/wxMediaCtrl2.h`.
|
||||
- `wxMediaCtrl2.cpp` (Win/Linux) stays in the build as-is (dead but harmless; out of
|
||||
scope to remove on this branch).
|
||||
|
||||
### 4. Verification
|
||||
|
||||
- Build on macOS: `cmake --build build_arm64` (or `build/arm64`).
|
||||
- Confirm no dynamic FFmpeg dependency: `otool -L` on the app binary shows no `libav*`
|
||||
dylib references.
|
||||
- Runtime: with the network plugin loaded, the Device tab camera preview streams via
|
||||
the FFmpeg player (check the device page / `MediaPlayCtrl`).
|
||||
- macOS `ctest` still passes — static linking means no test-executable `.so` copying
|
||||
hacks (unlike the Linux shared-lib setup).
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Linux (shared libs, AppImage/flatpak bundling) and Windows (prebuilt DLL zips)
|
||||
keep their current FFmpeg setup.
|
||||
- Retina-aware frame scaling / native CGImage rendering (follow-up if visual quality
|
||||
is judged insufficient).
|
||||
- Audio streaming (neither player plays audio in this UI path).
|
||||
Reference in New Issue
Block a user