An fcntl lock belongs to the process and goes with the first close of
any other descriptor to the lock file, so a backup or an export walking
the data dir could drop the guard's lock without a trace. On POSIX the
guard holds a flock on its own open file instead, which nothing else in
the process can release; Windows keeps LockFileEx.
The Windows write probe opened the target for writing and took a
sharing violation for a refusal, so a file another process merely held
open was not saved at all; only a real denial refuses now, since the
rename that follows moves an open destination aside. A file moved aside
by a refused rename may be the last copy of a file or a file since
deleted on purpose, so the sweep logs it and leaves it to the user
rather than removing or restoring it. The sweep throttles itself per
directory, so a load followed by a save reads each directory once, and
the identity check on the lock file runs at most once a second, so a
scan of hundreds of presets pays for it once. A config that stays
unwritable backs off for longer with each failure in a row, and its
backup copy is written after the config, never before. The Windows
identity helper is shared with the rename that already computed it, and
the header comment that had lost its indentation and its neighbour's
description is whole again.
The ten-second back-off after a failed config write sat inside save()
itself, so the save on exit could return without writing and lose the
session's changes; it gates only the idle-time save now, through
save_due(), and an explicit save always tries.
The preset scan's failure handlers removed a broken preset and its .info
after the per-file guard had ended, so another instance's fresh copy of
that file could be deleted from under it; they take the guard too. A
lock that is acquired ends the cool-down a timeout started, rather than
letting every guard for the rest of it proceed unlocked whenever the
lock is momentarily held. The lock file's identity is checked on every
acquisition, one stat, since a handle to a replaced file locks nothing
anyone else can see. Write access on Windows is probed with an open for
writing, which sees ACLs where _waccess sees only the read-only
attribute, and a temporary name too long for the filesystem falls back
to an in-place write for a new file as well as an existing one. The lock
tests restore the knobs they change however they end.
Nothing replaces a read-only file on Windows, neither a rename over it
nor an in-place write, so the opt-in that keeps a read-only config
saveable clears the attribute first; without that the test for it could
not pass there. A failed config write still leaves the flag dirty, as
before this change, but the next attempt waits ten seconds, so the idle
handler does not repeat a hopeless write on every event.
A file moved aside by a refused rename can be the only copy left if the
process dies in between, so the sweep puts such a file back when its
original is missing rather than removing it, and the directory-only
errors that could only come from a directory target no longer trigger
the move-aside at all. The sweep after a write runs only under the data
dir and at most once an hour per directory, so a batch of saves does not
read the directory once per file and an export into a user's folder
never reads that folder. A lock holder that never lets go doubles the
cool-down for each timeout in a row, up to five minutes, instead of
costing a stall every ten seconds for good.
Before this change a config whose temporary could not be written stayed
dirty, so the idle handler tried again; the last round cleared the flag
regardless, which lost a pending change to a transient failure. The
writer reports whether the config itself was written and the flag clears
only then, as it always did.
The Windows rename retry duplicated what WindowsSupport::rename already
does, retrying and moving an open destination aside, and multiplied its
error logging; it is gone. The file a refused rename moves aside is
named so the sweep can clear it after a crash in between, and every
successful atomic write sweeps leftovers of earlier writes to the same
target, so the caches and the smaller state files are covered too; the
name shapes are strict enough that nothing of the user's matches.
The lock wait defaults to a second, long against a critical section of
milliseconds and short against the GUI thread; a lock file that cannot
be opened says to check its owner and permissions; the test timing
bounds tolerate a loaded runner; and the helper's header includes what
its declarations use.
The two-step rename fallback removed the target before its second try,
and rename(2) reports the same errors for a source it cannot move, so a
refusal about the source cost the caller its existing file. The target
is moved aside and put back if the second step fails too.
The wait for a reader holding the target open was layered on the write
helper and ran on every platform and for every refusal, so a read-only
target on Windows cost half a second before failing anyway, while G-code
exports through rename_file() got no wait at all. It lives in
rename_file() now, on Windows only, and only for a target this process
could write. A failed config write clears the dirty flag as it always
did, so the idle handler does not repeat it on every tick.
The lock file is created readable and writable by every user, since
another user sharing the data dir has to open it read-write; the check
that the file behind the path is still the one opened runs at most every
few seconds rather than once per preset during a scan; the physical
printer loader reads under the lock; the stat headers join the existing
platform include block; and the utility tests keep their file's tag.
The config was always replaced by a rename, never written in place, so
the write helper's refusal of a read-only target, right for the presets
it mirrors, stopped a read-only OrcaSlicer.conf from ever being saved
again. The helper takes an explicit opt-in to replace such a file, and
the two config save variants share one writer that uses it.
The identity helper's stat headers were included only under the Linux
guard, which the Linux-only build never noticed; they are included on
every platform now. A temporary the longer name pushes past the path
limit is written in place like one refused for permissions, the rename
retry that waits for a Windows reader runs on Windows only, a failing
stat in the sweep skips that entry rather than ending the sweep, and the
rename fallback no longer stats the target it is about to remove.
Holding the lock across the whole preset scan meant a reload on a
background thread, which the login path runs, blocked a save on the GUI
thread for the scan's duration through the in-process mutex, which has
no timeout. Each file is locked on its own now, which keeps a file whole
under a reader without keeping the saver waiting.
A guard kept its handle to the lock file for good, so a lock file that
someone deleted or recreated left this instance locking a file no other
instance could see. The guard compares what the path names against what
it opened and reopens when they differ. Preset::save() serialises before
it takes the lock, so the exclusive window is the two file writes.
On Windows an unlocked reader, which the CLI and a timed-out instance
are by design, made the rename fail at once and the write go in place
under that reader; the rename is retried for half a second first, since
a reader is done in milliseconds, and the fallback when no temporary
can be created is logged like the other one. The sweep matches only the
exact <name>.<pid>.<n>.tmp shape and waits an hour, since hosts sharing a
data dir may disagree on the time. Real write access is checked with
access(), the read-only tests skip as root, and the dead permissions
block after the rename is gone.
The sweep's prefix form counted any <name>.<digits> file as a leftover of
the old config writer, so a user's OrcaSlicer.conf.1 backup went at the
next start. Only the <name>.<pid>.<n>.tmp form is a temporary now, and
the name is tested before the entry is stat'ed.
The read-only check went by mode bits, which misses a deny-write ACL on
Windows and refuses a root-owned file root can write; it asks the OS
with access() instead. The two-step rename fallback runs only for the
errors a refused replace produces, never for an I/O error that would
only lose the target, and says so if the second step fails after the
old file is gone.
The orphaned-.info scan on the sync thread reads each .info under the
lock, so a remove_files() in another instance is seen whole or not at
all, and queues the cloud delete after the lock is released, keeping the
queue mutex outside it. AppConfig::save() assembles its text before it
takes the lock, like Preset::save(). The two bundle loops share one
metadata loader, and the two-thread write test counts leftover
temporaries rather than every directory entry, since a scanner on
Windows may hold the old file under another name for a moment.
Sweeping the target's directory from inside write_file_atomically() made
every settings export into a user's folder delete their own numbered
files that matched the older config temporary form, and cost a directory
walk per save. The helper writes its target and nothing else; the user
preset scan, the bundle metadata reads and AppConfig::load() sweep the
directories the application owns, once, while they hold the lock.
Replacing a file needs only a writable directory, so a preset or config
the user made read-only was overwritten where the in-place write used to
fail; such a target is refused before anything is written. The CLI's
load_if_exists() takes no lock and creates no lock file, since the CLI
never saves. The lock guard holds the slot mutex through a unique_lock,
so an exception during construction cannot leave the slot locked for
good, and it counts its entry last so a throw leaves the slot as found;
the cool-down after a failed open is set where the failure is seen.
The cloud agent's sync state and secret fallback file and the 3DPrinterOS
session file wrote through a fixed ".tmp" name with a non-Unicode stream;
they call the helper. The vendor cache failure test makes the cache
read-only, which the helper refuses on every platform, and the utility
tests carry the PascalCase tag the test rules ask for.
Two threads writing one target without a lock shared the same temporary
name, so one could truncate it under the other; the name now carries a
per-call counter. The helper takes its content as chunks, which lets the
vendor cache hand over its header and body without copying the blob, and
AppConfig::save() goes through it too, including the Windows backup copy,
so the config's temporaries get the same handling as everyone else's.
Copying the target's permissions onto the temporary set a read-only bit
that stopped the rename itself on Windows; they are applied after the
rename there. The two-step rename fallback triggers on whatever error a
mount reports when the target is still there, since FUSE, gvfs and MTP
refuse a replacing rename with errors other than the three it handled.
Every successful write sweeps stale temporaries of its own target, so
leftovers beside the bundle metadata, physical printers, plugin config
and profile cache are covered, and the older OrcaSlicer.conf.<pid> form
with them. The bundle metadata guard is taken per file, never while the
bundle registry's writer lock is held, so the two are not taken in both
orders. The lock guard's open-and-lock steps are one block, and the
read-only choice lives in user_presets_lock_path() instead of at each
call site.
The vendor cache test that blocked the old fixed temporary name with a
directory provokes the failure through permissions instead.
Some mounts refuse to replace an existing file in one rename, and the
old remove-then-rename worked there where the atomic path now fails, so
rename_file() takes the two-step route when a one-step replace is
refused and write_file_atomically() writes in place whenever the rename
fails, not only for the Windows sharing case.
An instance that gave up waiting for the lock writes unlocked by design,
so a peer holding the lock could sweep its in-flight temporary and make
its rename fail; only temporaries older than ten minutes are removed now,
and the sweep uses the error-code overloads so an entry vanishing between
listing and stat cannot throw out of startup. The registry mutex is
leaked like the map it guards, so a save during static destruction does
not lock a destroyed mutex.
The physical printer loader is never called, so its guard is gone, while
the two delete paths that do run now hold the lock. The bundle metadata
loader is lock-free again, since the zip import reads it from a scratch
folder; the guard sits at the two scans that read the user's bundles.
Preset::save() builds what it writes before taking the lock, the cache
writer reserves its payload, and the retry test tolerates a slow runner.
A lock file that failed to open once stayed unopened for the rest of the
process, so a scanner holding the fresh file for a moment on Windows
silently disabled the lock for the session, and a lock call that throws,
as it does on a share without a lock service, was re-attempted and logged
by every guard. Both are now left alone for the cool-down and then retried.
Renaming a fresh file over the target turned a symlinked preset into a
plain file, reset its permissions to the umask default, and could not
work at all for a settings export to a device or pipe, since a temporary
cannot be created beside /dev/stdout. A target that is not a regular file
is now written in place, an existing target keeps its permissions, and a
directory that refuses the temporary but not the file falls back too.
The inline PhysicalPrinter::save() overload was the one physical printer
writer without the guard. load_info() took the lock redundantly under the
scan guard and, in read-only mode, created user.lock per file; the bundle
metadata reader now honours read_only too. The plugin config, the setup
guide's profile cache and the vendor preset cache wrote through a
temporary by hand; they use the helper, with a binary mode for the cache.
A crash between temporary and rename left a <name>.<pid>.tmp behind for
good; AppConfig::load() and the preset scan remove stale ones while they
hold the lock, and AppConfig::save() names its temporary the same way.
Windows refuses to replace a file that another process holds open without
FILE_SHARE_DELETE, which is how the C runtime opens files for reading, so
the atomic write could fail against a concurrent reader and drop the save
where the old in-place write had succeeded. Lock the readers that were
still outside the guard, Preset::reload() and the physical printer loader
and writer, and when the rename still fails that way, log it and write in
place as before; losing the save is worse than a torn read. Report the OS
error from a failed write instead of a generic I/O error.
Each leaf guard paid the full two-second wait on its own, so a bulk save
against an instance holding the lock for a long scan stalled once per
preset while holding the preset collection mutex. After a timed-out wait
the same lock file is not waited on again for ten seconds. Read-only scans
never rewrite or delete and many CLI jobs may share one data dir, so they
take no lock and no longer queue behind each other or log about writing.
Take the bundle metadata guard beside the write rather than while the JSON
is built, state the lock-order rule in the header, and give the tests a
temporary file rather than a directory, since the process keeps the lock
file open and a directory holding it cannot be removed on Windows.
Every running instance shares one OrcaSlicer.conf and one user preset
tree, and nothing kept their writers apart. Two instances saving at the
same moment, or the cloud preset sync thread writing while the GUI thread
saved, could interleave, and a reader in another instance could open a
preset JSON or .info file between truncate and close and get a partial
file, dropping that preset for the session with a parse error.
Add InstanceLock, a scoped guard that serialises the threads of one
process through a recursive mutex and other processes through an advisory
OS file lock, which the OS releases when its holder dies. It is best
effort: when the lock file cannot be created, or another instance still
holds it after two seconds, the guard logs a warning and lets the write
proceed rather than letting a hung instance block every other one.
AppConfig::save() and load() hold OrcaSlicer.conf.lock; load is included
because the Windows path restores from the .bak copy, and because Windows
cannot replace a file another process has open, so an unlocked reader
there made the other instance's save fail. Every user preset writer and
reader holds user.lock: Preset::save(), save_info(), load_info() and
remove_files(), the whole directory scan in load_presets(), the bundle
metadata file, the .info removal after a cloud-confirmed delete and the
bundle folder removal on unsubscribe. The guard sits at the leaf writers
on purpose: set_sync_info_and_save() calls save_info() under the preset
collection mutex, so a batch lock around save_user_presets(), which takes
that mutex through delete_preset(), would invert the order.
Write preset JSON, .info and bundle metadata files through a temporary
file beside the target that is renamed over it, so a reader that never
waits still sees a complete old or new file. On Linux and macOS
rename_file() deleted the target before renaming, leaving a window in
which the file did not exist at all, and returned a meaningless error
code on failure; rename() replaces atomically, so call it directly.
AppConfig::save() now logs a failed final rename instead of dropping the
save silently.
# Description
A plate using one filament no longer reserves or draws a prime tower in
the plater just because multiple filament slots are configured. The
shared footprint estimate gated "no tower" on the purge volume being
zero, but on non-Bambu printers with the shipped defaults that volume
comes from the SEMM flush matrix, which reads every configured slot and
stays nonzero even when only one filament is used — so the preview
showed a tower the print could never contain. The estimate now decides
from the filament count alone, with wrapping detection and smooth
timelapse staying the only reasons a single-filament plate keeps a
tower, matching `Print::has_wipe_tower()`. The gate also moved above the
flush-volume scan so the lone-filament path no longer pays for it.
No change to slicing output — toolpath generation already omitted the
tower for a single used filament; this removes the phantom preview,
placement reservation, and validation footprint.
# Screenshots/Recordings/Graphs
Before the fix:
https://github.com/user-attachments/assets/c5b0ea61-127d-408e-bafe-f1f76c58b996
After the fix:
https://github.com/user-attachments/assets/ac6632b3-5d44-4013-aca3-73f6cfd1139c
## Tests
<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->
Added a shipped-defaults single-filament case to
`tests/libslic3r/test_wipe_tower_estimate.cpp`: it estimates a 5.04
mm-deep tower before the fix and zero depth after. `[WipeTowerEstimate]`
and the fff_print `[WipeTower]` suites pass.
<!--
> A guide for users on how to download the artifacts from this PR.
-->
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
The no-tower case was gated on there being no purge volume, but the SEMM flush matrix reads every configured slot and is nonzero even when only one filament is used, so the plater preview drew a tower the print would never contain.
* Toolchange Cyclic Order
* Apply cyclic order to first layer
* Unit test
* Copilot fixes
---------
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
* Run every profile maintenance job from one tool
orca_id_tool.py becomes orca_profile_tool.py, and orca_extra_profile_check.py
and orca_filament_lib.py fold into it as subcommands: check, generate-id, fix,
trim, update-index and update-snapshot. The three scripts already overlapped --
the checker imported half of its rules from the id tool, which in turn kept a
copy-pasted set of output helpers to avoid the resulting import cycle -- while
disagreeing on how a vendor is enumerated, how a JSON file is read and what the
exit code means. One file settles all three.
check, normalize, trim and update-index reproduce their predecessors exactly; normalize and
update-index were diffed byte-for-byte against the old scripts over a copy of
the whole tree. Deliberate changes: the compatible-printers check no longer
switches itself off when --check-materials is passed, an error exits 1 rather
than -1, update-index honours --profile-type and reports a profile it cannot
place instead of dropping it from the index, fix and update-index gained
--dry-run, trim keeps an unindexed file some surviving profile still inherits
from, and vendors are enumerated as directories with an index -- which is why
blacklist.json, a data file that an unscoped index rebuild once wrote four empty
list sections into, loses them here and will not collect them again. The dead
rename_filament_system() helper is gone.
The suite under scripts/tests now covers the maintenance commands too, and CI
runs it; nothing there ran in CI before. No shipped profile data changes apart
from those four keys.
* update vendor index files with "python3 ./scripts/orca_profile_tool.py update-index" and "python3 ./scripts/orca_profile_tool.py normalize"
* CLI: --ground-face-* / --lay-flat / --center-on-bed orientation primitives
Adds the CLI counterparts to the GUI's lay-flat / face-pick gizmos.
Scripted / CI / AI pipelines can now set orientation without rendering
a wxWidgets frame; today the only way is a GUI round-trip.
New CLI actions (all operate in the mesh-local frame so they compose
with prior --rotate-* / --orient flags):
--ground-largest-face 1 Auto-detect the largest planar-face
or --lay-flat 1 cluster (area-weighted), rotate so its
normal points -Z. Covers "this part has
one obvious flat side" cases.
--ground-face-normal NX,NY,NZ Pick the face whose mesh-local
normal best matches the given
vector; ground it. e.g.
`--ground-face-normal 1,0,0`
stands a part on its +X side.
--ground-face-point X,Y,Z Find the triangle containing the
given mesh-local point; ground its
face. Disambiguates when several
faces share a normal (largest
containing triangle wins).
--center-on-bed 1 Translate so the XY bounding-box
centroid lands at the bed center
(derived from printable_area).
New file `src/slic3r/Utils/MeshOrient.{hpp,cpp}`:
- collect_triangles_object / compute_face_clusters — quantize
per-triangle normals (0.001, ~0.06°) and area-weighted-average
within clusters. Same clustering logic used by lay-flat.
- apply_ground_rotation — same math as Selection::flattening_rotate
in the GUI (Selection.cpp:1432): world-space quaternion from the
transformed normal to -Z, applied as offset * new_rot * old_no_offset
on every instance of every object, then a per-instance Z-lift so the
grounded face lands at exactly 0 (avoids "No layers were detected"
from FP-error z≈-1e-9).
- ground_face_point uses a top-N cluster search + point-in-triangle
test in local space; largest-area triangle wins on ambiguity.
Rationale: without these, any CLI pipeline that needs a specific
face on the bed must either encode custom rotation math per part or
break out of the pipeline into the GUI. Both are bad for
reproducibility. The --ground-face-* triple + the largest-face
auto-mode cover essentially every orientation intent expressible
in a slicing wizard.
Scope:
- `src/slic3r/Utils/MeshOrient.{hpp,cpp}` — new, ~420 lines
- `src/slic3r/CMakeLists.txt` — 2-line registration
- `src/libslic3r/PrintConfig.cpp` — 5 new CLIMiscConfigDef entries
- `src/OrcaSlicer.cpp` — 58-line handler block + 1 include
No behaviour change when the flags are absent.
(cherry picked from commit c45a9795e1)
* CLI grounding: choose among the Lay on Face planes, per object
Addresses review:
- Move the geometry of GLGizmoFlatten::update_planes() into
libslic3r/LayOnFace and use it from the gizmo and the CLI, so the
--ground-* options pick convex-hull faces per object and instance,
with part transformations (--rotate-x/y) applied.
- Drop --center-on-bed, the --lay-flat alias and MeshOrient; make
--ground-largest-face a coBool.
- Parse --ground-face-normal and --ground-face-point strictly. A point
that only some objects contain grounds those and leaves the others.
- Fold in --inspect-mesh from #14603, reporting the same planes.
- Tests in tests/libslic3r/test_lay_on_face.cpp: bounding boxes before
and after, rotate then ground, two objects, and a ribbed part whose
parallel inner faces outsum its base.
* CLI --inspect-mesh, --ground-face-*: reject missing input and empty values
- Without an input file or --load-assemble-list, --inspect-mesh printed
nothing and exited 0. Reject it up front with CLI_INVALID_PARAMS.
- An explicit empty --ground-face-normal or --ground-face-point was
silently ignored. Only options given on the command line reach the
transforms loop, so an empty value now fails the strict parse like any
other malformed value.
* fix: guard H2C per-filament array reads against short config arrays
The H2C tool-ordering, wipe-tower, and g-code export paths index per-filament
config arrays by filament/tool id. A config with fewer entries than the filament
count (partial or legacy projects, minimal test configs) makes these reads run
past the end of the vector: silent under a normal STL, but UB that aborts under
the flatpak build's bounds-checked STL (_GLIBCXX_ASSERTIONS).
Route the reads through the existing clamping accessors (get_at,
get_filament_category, is_in_same_extruder) and add a small clamp helper for
filament_change_length. The guards are no-ops when the arrays are sized to the
filament count, so correctly specified configs are unaffected.
* fix: size the grouping context's filament_info to the filament count
build_filament_group_context built model_info.filament_info by walking
filament_type, so a config whose filament_type is shorter than the filament
count produced a short vector. FilamentGroup indexes filament_info by filament
id, so clamping the individual reads only moved the out-of-bounds access
downstream. Loop to filament_nums and read all three fields through get_at,
and drop filament_ids entries past the filament count, since the grouping code
pairs filament_ids and filament_info by position.
Adds a regression test with four filaments and one-entry filament_type /
filament_is_support. Without the fix it throws bad_alloc from copying a garbage
std::string read past the end.
* fix: guard the carousel nozzle-change length reads too
The carousel branch added in b90ac13d86/b0dddb4648 reads
m_filaments_change_length by tool id without a bounds check, the same
pattern this branch already routed through filament_change_length_at
a few lines above in both plan_toolchange and plan_tower_new.
* fix: guard WipeTower per-filament array reads against short config arrays
The BambuStudio WipeTower sync reintroduced raw per-filament array
indexing that reads out of bounds when a config leaves an array shorter
than the filament count: m_physical_extruder_map in format_line_M104/M109
(indexed even when empty), and m_filament_categories in get_wall_skip_points
and get_wall_filament_for_all_layer. Silent on a normal STL, a hard abort
under the bounds-checked STL the Flatpak build uses.
Bounds-check the physical extruder map before indexing (omitting the T
token, as the existing -1 path already does), and route the two raw
m_filament_categories reads through the clamping get_filament_category()
accessor the surrounding code already uses. No change for correctly-sized
configs.
# Description
<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
> * What issue does this PR address or fix?
> * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->
Since #15438, every CLI run that loads a system preset spends about a
second per preset file re-parsing that vendor's entire profile tree. A
slice with a machine, process and filament preset got roughly 2.5 s
slower, and a four-filament slice roughly 4 s slower. This PR loads each
vendor tree once per run instead. Resolved presets and G-code are
unchanged.
The GUI never takes this path, and no release contains #15438, so the
regression only affects CLI runs on current dev and nightly builds. That
includes print farms, slicing services and plugins that call
`orca-slicer --slice`, and CI suites.
## Changes
### Why it was slow
`PresetBundle::resolve_preset_config` resolves a system preset through
its vendor manifest by loading the whole OrcaFilamentLibrary bundle and
the whole vendor tree from JSON, then picking the one preset out. The
CLI did that separately for every `--load-settings` and
`--load-filaments` file, on a fresh `PresetBundle` each time. With BBL
presets, a machine + process + filament run opened `BBL.json` three
times and read BBL's 2,879 profile files and the library's 512 three
times over.
### Load each vendor tree once
- `PresetBundle` keeps every vendor bundle its manifest path loads,
keyed by source root, vendor and substitution rule, and reuses them for
later resolutions on the same bundle.
- OrcaFilamentLibrary is cached the same way, so vendors under one root
share a single library load and the library's own presets resolve from
that same instance. A vendor bundle only reads from its base while
loading, so sharing it is safe.
- A failed or throwing load is not kept, so error reporting is
unchanged.
- The key includes the source root, so presets from two different
profile roots still resolve separately.
- The CLI resolves every system preset through one `PresetBundle` for
the whole run, instead of creating one per file.
The resolved configurations still come from the same canonical vendor
loader, so what a preset resolves to does not change. Only the CLI calls
`resolve_preset_config`, so a long-lived GUI bundle cannot end up
holding profile trees that later change on disk.
# Screenshots/Recordings/Graphs
<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->
CLI slice of a 20 mm cube with X1 Carbon system presets. Both builds get
the same datadir, best of 3, Linux. "Before" is this PR's base from CI.
| System presets loaded | Before | After |
|---|---|---|
| machine | 0.95 s | 0.87 s |
| machine + process | 1.67 s | 0.92 s |
| machine + process + 1 filament | 2.51 s | 0.97 s |
| machine + process + 4 filaments | 5.00 s | 0.99 s |
Files opened during the machine + process + filament run (`strace -e
openat`):
| | Before | After |
|---|---|---|
| `BBL.json` | 3 | 1 |
| `OrcaFilamentLibrary.json` | 3 | 1 |
| `system/BBL/**/*.json` | 8,634 | 2,880 |
| `system/OrcaFilamentLibrary/**/*.json` | 1,536 | 512 |
Peak memory did not rise: max RSS 306 MB → 286 MB for the three-preset
run, and 305 MB → 285 MB for four filaments. The "before" figure is an
AppImage, so part of that gap is probably packaging.
## Tests
<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->
- New test "Manifest-backed resolution reuses the vendor tree it already
loaded" in `tests/libslic3r/test_preset_bundle_loading.cpp`. It resolves
one preset, changes the parent profile on disk, then resolves a sibling.
The same bundle returns the value it already loaded, and a fresh bundle
picks up the change.
- New test "Manifest-backed resolution shares the library between
vendors under one root". It resolves through one vendor, changes a
library profile on disk, then resolves through a second vendor and a
library preset on the same bundle. Both return the value already loaded,
and a fresh bundle picks up the change.
- All `[Preset][Bundle]` tests pass (87 test cases, 1,069 assertions),
including the existing manifest-backed resolution cases for source-root
scoping, malformed vendor loads, missing parents and type mismatches.
- G-code of the three-preset slice is identical before and after, header
lines excluded.
- The external CLI regression suite passes. Two cases report as
unexpectedly passing because #15639 fixed their bug. They pass the same
way on this PR's base without the change.
- A GUI-vs-CLI parity run over 10 fixtures shows no new differences.
- Builds clean on Linux (Release, with tests).
<!--
> A guide for users on how to download the artifacts from this PR.
-->
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
The manifest resolver loaded OrcaFilamentLibrary once per vendor it
resolved through, so a run that mixes vendors parsed the library tree
again for each of them. The library is now cached like any other vendor
tree, keyed on its root and substitution rule, and doubles as the base
every vendor under that root loads against. A vendor bundle only reads
from its base while loading, so sharing the instance is safe.
The cache key carries the substitution rule as its enum, and the lookup
lambdas take a const bundle since they only read.
`--export-settings` already writes the merged config as JSON at the
right point in the CLI flow. Passing `-` now writes that same document
to stdout, so scripts can inspect the effective config without a temp
file. This replaces #14605.
- `ConfigBase::save_to_json` gains a stream overload. The file overload
serializes through it before opening the file, so the output format is
unchanged, and a config that cannot be serialized (invalid UTF-8) now
leaves the existing file untouched instead of truncating it.
- On stdout, invalid UTF-8 in string values is written as U+FFFD instead
of ending the process with an uncaught `type_error`. Files keep the
strict behaviour.
- To keep stdout pure JSON, `-` is rejected up front (stderr message,
`CLI_INVALID_PARAMS`, shell status 254) when combined with an action or
transform that can write to stdout or does real work: `--info`,
`--help`, `--orient`, slicing and exporting. Options that do nothing
without a slice (`--uptodate`, `--min-save`, `--pipe`, ...) are still
accepted.
- The one unconditional stdout write on a success path, "skip locked
instance" during arrange, now goes to the log.
- Every other value, including the default `output.json`, behaves as
before.
Tests in `tests/libslic3r/test_config.cpp`: the stream output equals the
file output and keeps the tab-indented format; invalid UTF-8 throws on
the strict path and is replaced when asked; a failed save leaves the
previous file intact.
Opening a model with a relative path, for example `orca-slicer ./some.3mf`,
failed with "Loading of a model file failed." and "The file does not contain
any geometry data.", while the same file opened by an absolute path or by
drag and drop worked.
GUI_App::init_app_config() changes the working directory to <data_dir>/log,
and it runs from the GUI_App constructor because the app config is needed
early for instance checking. The input files are opened much later, in
post_init(), so a path still relative at that point resolved against the log
directory instead of the directory OrcaSlicer was started from, and the 3MF
reader failed to open it.
Resolve the input paths in CLI::setup(), which runs before GUI_App is
constructed and therefore before the working directory moves. Absolute paths
are returned unchanged, so the forms that open today are unaffected, and
custom open protocol URLs are passed through since post_init() hands those to
the downloader rather than the file loader.
The working directory change is left alone. It was added in #3248 so the TUTK
logs land in the data directory instead of the working directory (#3209).
--export-settings already writes the merged config as JSON at the right
point in the CLI flow. Passing - writes the same document to stdout.
- ConfigBase::save_to_json gains a stream overload. The file overload
serializes through it before opening the file, so the format is
unchanged and a config that cannot be serialized leaves an existing
file untouched instead of truncating it.
- On stdout, invalid UTF-8 in string values is written as U+FFFD instead
of ending the process with an uncaught type_error; files keep the
strict behaviour.
- - is rejected up front when combined with an action or transform that
can write to stdout or does real work, so stdout carries only the
JSON.
- The unconditional "skip locked instance" stdout write during arrange
now goes to the log.
- Tests in tests/libslic3r/test_config.cpp.
Cover the two cache paths the first test left open: a vendor tree that fails to load is retried on the next resolution instead of being served from the cache, and a type-probed filament resolved through resolve_preset_config_type reuses the OrcaFilamentLibrary base already loaded for a sibling.
Resolving a system preset through its vendor manifest loaded the whole vendor tree and the filament library from JSON, and the CLI did that separately for every --load-settings and --load-filaments file. A run with machine, process and filament presets parsed BBL's 2,879 profile files and the library's 512 three times over, about a second each.
Keep the library and vendor bundles loaded by the manifest path on the PresetBundle that resolved them, keyed by source root, vendor and substitution rule, and have the CLI resolve every system preset through one bundle for the whole run. A failed load is not kept, so errors are reported as before.
On a cube slice with X1C machine, process and PLA presets: 2.42 s -> 0.93 s, BBL.json opened once instead of three times, identical G-code.
update_values_to_printer_extruders_for_multiple_filaments picks each
filament's value from the flattened (filament x variant) columns of every
per-filament variant option. When a column index fell past the end of the
option's values, it skipped that filament and left the zero the output
vector was created with.
The GUI always hands this function full columns, but the CLI does not:
- a CLI override of a single value, such as --nozzle-temperature=211 on a
four-filament project, came out as 211,0,0,0, so three filaments would
print at 0 C;
- loading fewer filament presets than the project has filaments left the
remaining filaments' columns missing, so filament_cooling_before_tower
came out as 10,10,0,0 and filament_ramming_volumetric_speed as -1,-1,0,0.
An out-of-range column now keeps the option's first value, the fallback
get_at() and the sibling gather step already use. The seven per-type copies
of the loop are replaced by that same gather_option_values helper, moved
above the function; it now takes its caller's name for its log lines. An
empty option, which has no first value, is given one registered default per
filament first; it used to be replaced with zeros.
On a partial load a filament whose preset was not loaded takes the first
filament's value rather than its own preset's, which the CLI does not load;
for the options seen in practice those agree.
* build: clear 2 warnings - cast the NSTextField the class check already proved
mainframe_text_field is NSTextField* and was assigned a bare NSView*, which
Clang reports as -Wincompatible-pointer-types. Both assignments sit inside
if ([viewObject class] == [NSTextField self]), so the runtime type is already
guaranteed, and the line above the second one casts the same variable the same
way to call setTextColor. macOS only, since nothing else compiles this file.
* build: clear 6 warning categories from the clang-cl inventory
-Wmissing-braces (9). Aggregates whose first member is itself an aggregate.
GUID's fourth member is BYTE[8], so the trailing eight bytes take their own
braces. The others were reaching for zero-initialization with {0} and say {}
now. bbs_3mf's backup Task ends in an anonymous union, which needs braces of
its own; those braces initialize the union's first member rather than the one
named at the call site, so the RemoveBackup site says so in a comment.
-Wmacro-redefined (11). SendMultiMachinePage.hpp defines five names that
Preferences.hpp, PresetBundleDialog.hpp, ExportPresetBundleDialog.hpp and
TroubleshootDialog.hpp also define with different values, so the value in
force depended on include order. All nine of this file's DESIGN_ macros take
the SEND_ prefix it already uses for its own macros, values unchanged, so a
DESIGN_ name added elsewhere later cannot collide with it again. They read as
one page-local palette, a 900 to 400 gray ramp plus sizes, so the four with
no current readers stay: dropping them would leave gaps in a named scale. test_marchingsquares.cpp defines NOMINMAX,
which libslic3r already passes as a PUBLIC compile definition, so it takes
the #ifndef guard the other suites use.
-Wbraced-scalar-init (3). Two PushStyleVar calls resolve to the float
overload, so the braces were initializing a scalar. ConfigOptionFloatsNullable
already takes an initializer_list, so the inner braces did the same thing.
-Wmicrosoft-goto (2). Both gotos in copy_file_gui jump forward over the
initialization of size, dwRead and dwWrite, which only MSVC accepts. Those
declarations move up to join the others at the top of the function.
-Wunused-private-field (3). Every use of ColourPicker's m_clrData and
m_picker_widget is behind !defined(__linux__), so on Linux they are written
and never read; the members now carry the same guard. ParamsPanel's
m_size_move is read nowhere. Tab has its own, which is the one Tab.cpp uses.
-Wnonportable-include-path (2). BaseException.h asked for "stackwalker.h"
and the file on disk is StackWalker.h.
fix: report the real error when a Windows G-code export fails
copy_file built its failure message as "Error: " + errCode. Adding a DWORD
to a string literal is pointer arithmetic, not concatenation, so the pointer
lands errCode bytes into an 8-byte literal and runs past its end for any code
above 7. std::string then calls strlen on it and throws length_error, and the
catch(...) in BackgroundSlicingProcess::finalize_gcode replaces the diagnosis
with "Unknown error occurred during exporting G-code."
Every code a user is likely to hit is past the end: write-protected media is
19, no media 21, a full disk 112, and a destination held open by another
program 32. Codes 1 to 7 stay inside the literal and produce a truncated
message instead. So the "Maybe the SD card is write locked?" text has not
been reachable on Windows since this path was added in #2923.
Now that it is reachable, that guess only fits removable media, so it is
conditional on m_export_path_on_removable_media. The existing string is
untouched and keeps its 23 translations; the fixed-drive case adds one string.
The preview brim, the placement margin and the pre-generation validation
warning each decided on their own whether the tower has a Type2 cone
base, reading the wall type and cone angle three different ways. The
preview's read cast the preset's enum to ConfigOptionEnum<T>, which a
preset-shaped config never holds, so the cone base was never previewed.
estimate_wipe_tower_first_layer_outline now answers that question once,
beside the footprint estimate, from the config and the resolved planner;
all three sites take the outline from it. The libslic3r case reads the
outline off a preset-shaped config, where the old cast came back empty.
Smooth timelapse no longer charges a prime volume it does not purge. A
tower printed with no tool change is exactly the idle depth: the
stability minimum for Type2, the wrapping detection depth for Type1.
Charging a full prime_volume on top made the previewed and arranged
tower deeper than the one that is printed.
The Type2 half of "a tool change reserves a tower whatever the purge
volumes resolve to" arrives with the base commit; here it only has to
survive the planner split, since Type1 already reserves per filament.
The wipe tower filament only joins the tool ordering when there is a
tower to join, which is the has_wipe_tower() half of the guard
Print::extruders applies.