Compare commits

...

30 Commits

Author SHA1 Message Date
ExPikaPaka
c014f0403b Add a bit more tests 2026-07-06 10:20:13 +02:00
ExPikaPaka
3765549786 Add tests for Cache system 2026-07-06 09:53:58 +02:00
ExPikaPaka
d7bacd5c1c Minimize field duplication by moving Cache thing into PresetBundle 2026-07-06 08:37:32 +02:00
ExPikaPaka
fd3bee3c62 Serealize all value fields for Preset class to minimize regression later 2026-07-06 08:16:23 +02:00
ExPikaPaka
e5ed64a307 Update check for stale cache 2026-07-03 07:46:51 +02:00
ExPikaPaka
47fd55d462 Revert json cache back 2026-07-03 07:46:06 +02:00
SoftFever
b90439adba Merge branch 'main' into feature/cache_profiles_and_optimize_loading_speed 2026-07-01 16:21:11 +08:00
ExPikaPaka
31270d4027 Fix build for windows arm64 2026-07-01 08:50:58 +02:00
Rodrigo Faselli
c9afb98ca2 Merge branch 'main' into feature/cache_profiles_and_optimize_loading_speed 2026-06-28 21:27:14 -03:00
Rodrigo Faselli
d102157138 Merge branch 'main' into feature/cache_profiles_and_optimize_loading_speed 2026-06-26 07:47:50 -03:00
ExPikaPaka
f088a18167 Remove leftover cache file 2026-06-18 10:38:05 +02:00
ExPikaPaka
f2f5bea4bf Skip invalid vendors 2026-06-18 10:37:04 +02:00
ExPikaPaka
0cd9e77e95 Remove BOM added by VSC 2026-06-18 08:52:42 +02:00
ExPikaPaka
f35c2b1ef7 Use get_vendor_cache_key() to match cache keys written by the app 2026-06-18 08:35:13 +02:00
ExPikaPaka
97dee9349b Fix use-after-free in CallAfter lambda; replace raw thread pointer with unique_ptr 2026-06-18 08:35:03 +02:00
ExPikaPaka
493597f132 Remove CachedPrinterModel/VendorProfile/Preset mirror structs from VendorCache 2026-06-18 08:34:50 +02:00
ExPikaPaka
517fa29d6f Add cereal serialize() to VendorProfile, PrinterModel, Preset, and Semver 2026-06-18 08:34:27 +02:00
ExPikaPaka
2ab9e14525 Simplify code a bit more 2026-06-17 09:17:07 +02:00
ExPikaPaka
b6a1546ff5 Merge branch 'feature/cache_profiles_and_optimize_loading_speed' of https://github.com/OrcaSlicer/OrcaSlicer into feature/cache_profiles_and_optimize_loading_speed 2026-06-17 08:50:38 +02:00
ExPikaPaka
6e36411736 Simplify code by mergin it in PresetBundle 2026-06-17 08:46:06 +02:00
ExPikaPaka
0ec6c03c83 Generate cache per vendor 2026-06-17 08:45:24 +02:00
SoftFever
7a8f5a88c9 Merge branch 'main' into feature/cache_profiles_and_optimize_loading_speed 2026-06-16 16:27:46 +08:00
ExPikaPaka
83e1712ded Add inspecting tool and fix CI cache generation 2026-06-16 08:57:33 +02:00
ExPikaPaka
80fbf3b405 Add cache to GuideDialog as previos version didn't work as expected 2026-06-16 08:57:09 +02:00
ExPikaPaka
5b80d0cc07 Handle corrupted files 2026-06-16 08:56:25 +02:00
ExPikaPaka
88901a969f Add partial cache generation when only one of the vendros is changed to speed up recalculation time 2026-06-15 09:28:59 +02:00
ExPikaPaka
5d0c640f7b Add CI\CD step to prepare cache file in ahead of time so user does not need to wait 2026-06-15 09:19:53 +02:00
ExPikaPaka
d861e8af22 Integrate caching into WebGuideDialog which speeds up time of SetupWizzard and PrinterSelection dialog 2026-06-15 08:52:55 +02:00
ExPikaPaka
6186436b23 Removing user\bundle serialization and keeping it only for system presets 2026-06-15 08:31:33 +02:00
ExPikaPaka
604f15e20d Add caching system for presets 2026-06-11 08:52:44 +02:00
15 changed files with 1724 additions and 114 deletions

View File

@@ -144,6 +144,15 @@ jobs:
run: |
./build_release_macos.sh -s -n -x ${{ !vars.SELF_HOSTED && '-1' || '' }} -a ${{ inputs.arch }} -t 10.15
- name: Generate system presets cache (macOS)
if: runner.os == 'macOS' && !inputs.macos-combine-only
working-directory: ${{ github.workspace }}
shell: bash
run: |
tool=$(find build/${{ inputs.arch }} -name generate_system_cache -type f | head -1)
profiles=$(find build/${{ inputs.arch }} -path "*/Resources/profiles" -type d | head -1)
"$tool" --path "$profiles" --log_level 2
- name: Pack macOS app bundle ${{ inputs.arch }}
if: runner.os == 'macOS' && !inputs.macos-combine-only
working-directory: ${{ github.workspace }}
@@ -340,6 +349,23 @@ jobs:
if ($arch -eq "arm64") { .\build_release_vs.bat slicer arm64 } else { .\build_release_vs.bat slicer }
shell: pwsh
- name: Generate system presets cache (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$buildDir = $env:BUILD_DIR
$tool = Get-ChildItem -Recurse -Path $buildDir -Filter "generate_system_cache.exe" | Select-Object -First 1
if (-not $tool) { Write-Error "generate_system_cache.exe not found in $buildDir"; exit 1 }
$profiles = Get-ChildItem -Recurse -Path $buildDir -Directory -Filter profiles |
Where-Object { $_.FullName -match 'resources' } | Select-Object -First 1
if (-not $profiles) { Write-Error "profiles directory not found in $buildDir"; exit 1 }
# Add the slicer's runtime DLL directory to PATH so generate_system_cache.exe
# can resolve its dependencies (TKernel.dll etc.) without a full install step.
$dll_dir = Get-ChildItem -Recurse -Path $buildDir -Filter "TKernel.dll" |
Select-Object -First 1 | Select-Object -ExpandProperty DirectoryName
if ($dll_dir) { $env:PATH = "$dll_dir;$env:PATH" }
& $tool.FullName --path $profiles.FullName --log_level 2
# NSIS is x86-only; it runs (and the installer it emits runs) under ARM64's
# x86 emulation, packaging the native arm64 payload from build-arm64.
- name: Create installer Win
@@ -477,6 +503,22 @@ jobs:
retention-days: 5
if-no-files-found: error
- name: Generate system presets cache (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
tool=$(find build -name generate_system_cache -type f | head -1)
if [ -z "$tool" ]; then echo "ERROR: generate_system_cache not found in build tree" >&2; exit 1; fi
"$tool" --path build/package/resources/profiles --log_level 2
# Re-pack the AppImage so the per-vendor caches are included
appimage=$(find build -maxdepth 1 -name "OrcaSlicer_Linux_AppImage*.AppImage" | head -1)
chmod +x "$appimage"
"$appimage" --appimage-extract
cp build/package/resources/profiles/*.cache squashfs-root/resources/profiles/
appimagetool=$(find build -name "appimagetool.AppImage" | head -1)
ARCH=$(uname -m) "$appimagetool" --appimage-extract-and-run squashfs-root "$appimage"
rm -rf squashfs-root
- name: Run external slicer regression tests
if: runner.os == 'Linux' && inputs.arch != 'aarch64'
timeout-minutes: 20

View File

@@ -567,6 +567,8 @@ if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then
print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target OrcaSlicer
echo "Building OrcaSlicer_profile_validator .."
print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target OrcaSlicer_profile_validator
echo "Building generate_system_cache ..."
print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target generate_system_cache
./scripts/run_gettext.sh
fi
if [[ -n "${BUILD_TESTS}" ]] ; then

View File

@@ -111,6 +111,8 @@ if(ORCA_TOOLS)
endif()
target_link_libraries(OrcaSlicer_profile_validator libslic3r boost_headeronly libcurl OpenSSL::SSL OpenSSL::Crypto)
target_compile_definitions(OrcaSlicer_profile_validator PRIVATE -DBOOST_ALL_NO_LIB -DBOOST_USE_WINAPI_VERSION=0x602 -DBOOST_SYSTEM_USE_UTF8)
endif()
# Create a slic3r executable

View File

@@ -20,6 +20,16 @@ if (SLIC3R_ENC_CHECK)
)
endif()
if (ORCA_TOOLS)
set(_DEV_DEFS -DBOOST_ALL_NO_LIB -DBOOST_USE_WINAPI_VERSION=0x602 -DBOOST_SYSTEM_USE_UTF8)
# generate_system_cache: pre-generates per-vendor resources/profiles/<id>.cache files for CI bundling.
add_executable(generate_system_cache generate_system_cache.cpp)
target_link_libraries(generate_system_cache libslic3r boost_headeronly)
target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS})
endif()
# Function that adds source file encoding check to a target
# using the above encoding-check binary

View File

@@ -0,0 +1,126 @@
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <boost/program_options.hpp>
#include <boost/system/error_code.hpp>
#include <iostream>
using namespace Slic3r;
namespace fs = boost::filesystem;
namespace po = boost::program_options;
int main(int argc, char* argv[])
{
po::options_description desc("OrcaSlicer System Cache Generator\nUsage");
// clang-format off
desc.add_options()
("help,h", "Show help")
#ifdef __APPLE__
("path,p", po::value<std::string>()->default_value("../../../../../../../resources/profiles"), "Path to profiles directory")
#else
("path,p", po::value<std::string>()->default_value("../../../resources/profiles"), "Path to profiles directory")
#endif
("log_level,l", po::value<int>()->default_value(2), "Log level (0=trace, 2=info, 4=error)");
// clang-format on
po::variables_map vm;
try {
po::store(po::parse_command_line(argc, argv, desc), vm);
if (vm.count("help")) { std::cout << desc << "\n"; return 0; }
po::notify(vm);
} catch (const po::error& e) {
std::cerr << "Error: " << e.what() << "\n" << desc << "\n";
return 1;
}
const std::string profiles_path = vm["path"].as<std::string>();
const int log_level = vm["log_level"].as<int>();
if (!fs::exists(profiles_path) || !fs::is_directory(profiles_path)) {
std::cerr << "Error: '" << profiles_path << "' is not a valid directory\n";
return 1;
}
set_logging_level(log_level);
// In validation_mode, load_system_presets_from_json uses data_dir() directly
// (no /system/ suffix), so point data_dir at the profiles directory.
set_data_dir(profiles_path);
set_resources_dir(fs::path(profiles_path).parent_path().make_preferred().string());
// load_presets creates user preset dirs under data_dir().
const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR;
if (!fs::exists(user_dir))
fs::create_directories(user_dir);
AppConfig app_config;
app_config.set("preset_folder", "default");
auto preset_bundle = std::make_unique<PresetBundle>();
preset_bundle->set_is_validation_mode(true);
preset_bundle->set_default_suppressed(true);
std::cout << "Loading system presets from: " << profiles_path << "\n";
try {
preset_bundle->load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSilent);
} catch (const std::exception& ex) {
std::cerr << "Failed to load presets: " << ex.what() << "\n";
return 1;
}
// Collect all vendor names from JSON files in the profiles directory.
std::vector<std::string> vendor_names;
for (const auto& e : fs::directory_iterator(profiles_path)) {
if (e.path().extension() == ".json")
vendor_names.push_back(e.path().stem().string());
}
// Sort: PresetBundle::ORCA_FILAMENT_LIBRARY first, rest alphabetical.
std::sort(vendor_names.begin(), vendor_names.end(),
[](const std::string& a, const std::string& b) {
if (a == PresetBundle::ORCA_FILAMENT_LIBRARY) return true;
if (b == PresetBundle::ORCA_FILAMENT_LIBRARY) return false;
return a < b;
});
size_t total_print = 0, total_filament = 0, total_printer = 0;
int saved = 0, failed = 0;
for (const auto& vendor_name : vendor_names) {
try {
const std::string json_path = (fs::path(profiles_path) / (vendor_name + ".json")).string();
const std::string cache_path = (fs::path(profiles_path) / (vendor_name + ".cache")).make_preferred().string();
const bool is_orca_lib = (vendor_name == PresetBundle::ORCA_FILAMENT_LIBRARY);
const auto stats = preset_bundle->save_bundled_vendor_cache(vendor_name, json_path, is_orca_lib, cache_path);
if (!stats.ok) {
std::cerr << "ERROR: " << vendor_name << ": verification failed\n";
++failed;
} else {
std::cout << " [ok] " << vendor_name << ".cache"
<< " (" << stats.print_presets << " print, "
<< stats.filament_presets << " filament, "
<< stats.printer_presets << " printer)\n";
total_print += stats.print_presets;
total_filament += stats.filament_presets;
total_printer += stats.printer_presets;
++saved;
}
} catch (const std::exception& ex) {
std::cerr << "ERROR: " << vendor_name << ": " << ex.what() << "\n";
++failed;
}
}
std::cout << "\nDone: " << saved << " cache(s) written";
if (failed) std::cout << ", " << failed << " FAILED";
std::cout << "\n"
<< " Total print presets: " << total_print << "\n"
<< " Total filament presets: " << total_filament << "\n"
<< " Total printer presets: " << total_printer << "\n";
return failed ? 1 : 0;
}

View File

@@ -147,6 +147,20 @@ Semver get_version_from_json(std::string file_path)
return Semver();
//throw ConfigurationError(format("Failed loading configuration file \"%1%\": %2%", file_path, err.what()));
}
catch(...) {
return Semver();
}
}
std::string get_vendor_cache_key(const std::string& json_path)
{
const Semver ver = get_version_from_json(json_path);
if (ver.valid())
return ver.to_string();
// No version field — use mtime as change fingerprint so edits invalidate the cache.
boost::system::error_code ec;
const std::time_t mtime = boost::filesystem::last_write_time(json_path, ec);
return ec ? std::string{} : ("mtime:" + std::to_string(mtime));
}
//BBS: add a function to load the key-values from xxx.json
@@ -742,6 +756,7 @@ void Preset::save(DynamicPrintConfig* parent_config)
idx_file.replace_extension(".info");
this->save_info(idx_file.string());
}
}
void Preset::reload(Preset const &parent)

View File

@@ -16,6 +16,14 @@
#include "Semver.hpp"
#include "ProjectTask.hpp"
#include <cereal/archives/binary.hpp>
#include <cereal/cereal.hpp>
#include <cereal/types/map.hpp>
#include <cereal/types/polymorphic.hpp>
#include <cereal/types/set.hpp>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
//BBS: change system directories
#define PRESET_SYSTEM_DIR "system"
#define PRESET_USER_DIR "user"
@@ -113,6 +121,10 @@ extern Semver get_version_from_json(std::string file_path);
//BBS: add a function to load the key-values from xxx.json
extern int get_values_from_json(std::string file_path, std::vector<std::string>& keys, std::map<std::string, std::string>& key_values);
// Returns the cache key for a vendor JSON: the Semver string for versioned
// vendors, or "mtime:<unix_timestamp>" for vendors without a version field.
extern std::string get_vendor_cache_key(const std::string& json_path);
extern ConfigFileType guess_config_file_type(const boost::property_tree::ptree &tree);
extern void extend_default_config_length(DynamicPrintConfig& config, const bool set_nil_to_default, const DynamicPrintConfig& defaults);
@@ -130,6 +142,9 @@ public:
PrinterVariant() {}
PrinterVariant(const std::string &name) : name(name) {}
std::string name;
template<class Archive>
void serialize(Archive& ar) { ar(name); }
};
struct PrinterModel {
@@ -160,6 +175,15 @@ public:
}
const PrinterVariant* variant(const std::string &name) const { return const_cast<PrinterModel*>(this)->variant(name); }
template<class Archive>
void serialize(Archive& ar)
{
ar(id, name, model_id, family, technology, variants, default_materials,
not_support_bed_types, bed_model, bed_texture, image_bed_type,
bottom_texture_end_name, use_double_extruder_default_texture,
bottom_texture_rect, middle_texture_rect, hotend_model);
}
};
std::vector<PrinterModel> models;
@@ -171,6 +195,13 @@ public:
bool valid() const { return ! name.empty() && ! id.empty() && config_version.valid(); }
template<class Archive>
void serialize(Archive& ar)
{
ar(id, name, config_version, config_update_url, changelog_url,
models, default_filaments, default_sla_materials);
}
// Load VendorProfile from an ini file.
// If `load_all` is false, only the header with basic info (name, version, URLs) is loaded.
static VendorProfile from_ini(const boost::filesystem::path &path, bool load_all=true);
@@ -418,12 +449,31 @@ public:
// BBS: move constructor to public
Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {}
protected:
// Default constructor is public so cereal can default-construct elements when
// deserializing std::vector<Preset> (std::allocator is not a cereal::access friend).
Preset() = default;
protected:
friend class PresetCollection;
friend class PresetBundle;
friend class cereal::access;
// Serializes all value fields of Preset for the binary vendor cache.
// Raw pointers (vendor, loading_substitutions) are excluded — vendor is reconstructed
// by apply_vendor_cache() from the VendorProfile stored alongside the presets.
template<class Archive>
void serialize(Archive& ar)
{
ar(type, name, alias, file, version,
filament_id, setting_id, description,
renamed_from, is_system, is_visible,
is_default, is_external, is_dirty, is_compatible,
is_project_embedded, loaded,
m_from_orca_filament_lib, m_excluded_from,
bundle_id, user_id, base_id, sync_info,
updated_time, key_values, ini_str,
config);
}
};
bool is_compatible_with_print (const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer);

View File

@@ -1,7 +1,15 @@
#include <cassert>
#include <chrono>
#include <ctime>
#include <sstream>
#include "PresetBundle.hpp"
#include <boost/crc.hpp>
#include <cereal/archives/binary.hpp>
#include <cereal/types/map.hpp>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
#include "PrintConfig.hpp"
#include "libslic3r.h"
#include "I18N.hpp"
@@ -520,6 +528,8 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, Forward
//BBS: add config related logs
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, substitution_rule %1%, preferred printer_model_id %2%")%substitution_rule%preferred_selection.printer_model_id;
const auto startup_t0 = std::chrono::steady_clock::now();
//BBS: change system config to json
std::tie(substitutions, errors_cummulative) = this->load_system_presets_from_json(substitution_rule);
@@ -539,6 +549,12 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, Forward
set_calibrate_printer("");
{
const auto total_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - startup_t0).count();
BOOST_LOG_TRIVIAL(info) << "PresetBundle: all presets loaded in " << total_ms << " ms";
}
//BBS: add config related logs
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" finished, returned substitutions %1%")%substitutions.size();
return substitutions;
@@ -947,6 +963,8 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
bundles.m_bundles.clear();
bundles.WriteUnlock();
const auto user_load_t0 = std::chrono::steady_clock::now();
// Load bundle metadata from _local directory first
fs::path local_dir(folder / PRESET_LOCAL_DIR);
if (fs::exists(local_dir)) {
@@ -965,7 +983,6 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
metadata.filament_presets.clear();
metadata.printer_presets.clear();
// Add the profiles
this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.print_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id));
@@ -1002,7 +1019,6 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
metadata.printer_presets.clear();
metadata.is_subscribed = true;
// Load presets from bundle (same logic as __local__)
this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.print_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id));
@@ -1023,34 +1039,41 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
}
}
// BBS do not load sla_print
// BBS: change directoties by design
try {
std::string print_selected_preset_name = prints.get_selected_preset().name;
this->prints.load_presets(dir_user_presets, PRESET_PRINT_NAME, substitutions, substitution_rule);
prints.select_preset_by_name(print_selected_preset_name, false);
} catch (const std::runtime_error &err) {
errors_cummulative += err.what();
// BBS: change directories by design
{
const auto json_t0 = std::chrono::steady_clock::now();
try {
std::string sel = prints.get_selected_preset().name;
this->prints.load_presets(dir_user_presets, PRESET_PRINT_NAME, substitutions, substitution_rule);
prints.select_preset_by_name(sel, false);
} catch (const std::runtime_error& err) { errors_cummulative += err.what(); }
try {
std::string sel = filaments.get_selected_preset().name;
this->filaments.load_presets(dir_user_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule);
filaments.select_preset_by_name(sel, false);
} catch (const std::runtime_error& err) { errors_cummulative += err.what(); }
try {
std::string sel = printers.get_selected_preset().name;
this->printers.load_presets(dir_user_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule);
printers.select_preset_by_name(sel, false);
} catch (const std::runtime_error& err) { errors_cummulative += err.what(); }
if (!errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative);
const auto json_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - json_t0).count();
BOOST_LOG_TRIVIAL(info) << "PresetBundle: user presets loaded from JSON in " << json_ms << " ms";
}
try {
std::string filament_selected_preset_name = filaments.get_selected_preset().name;
this->filaments.load_presets(dir_user_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule);
filaments.select_preset_by_name(filament_selected_preset_name, false);
} catch (const std::runtime_error &err) {
errors_cummulative += err.what();
{
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - user_load_t0).count();
BOOST_LOG_TRIVIAL(info) << "PresetBundle: user + bundle presets loaded in " << ms << " ms";
}
try {
std::string printer_selected_preset_name = printers.get_selected_preset().name;
this->printers.load_presets(dir_user_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule);
printers.select_preset_by_name(printer_selected_preset_name, false);
} catch (const std::runtime_error &err) {
errors_cummulative += err.what();
}
if (!errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative);
this->update_multi_material_filament_presets();
this->update_compatible(PresetSelectCompatibleType::Never);
set_calibrate_printer("");
return PresetsConfigSubstitutions();
@@ -2183,9 +2206,71 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
if (validation_mode)
dir = (boost::filesystem::path(data_dir())).make_preferred();
// Per-vendor binary cache: try user cache, then bundled cache, then JSON parse.
// Vendors that hit a cache are applied immediately; misses go through JSON parsing below.
std::set<std::string> cache_miss_vendors;
std::map<std::string, std::string> vendor_json_versions; // vendor_id → version string on disk
if (!validation_mode) {
const auto t0 = std::chrono::steady_clock::now();
this->reset(false);
bool all_from_cache = true;
// Collect all vendor names from the system directory.
std::vector<std::string> all_vendor_names;
try {
for (const auto& e : boost::filesystem::directory_iterator(dir)) {
if (Slic3r::is_json_file(e.path().string()))
all_vendor_names.push_back(e.path().stem().string());
}
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << "PresetBundle: cannot scan system dir: " << ex.what();
}
// Load ORCA_FILAMENT_LIBRARY first (other vendors' filaments inherit from it).
// Then load remaining vendors from per-vendor caches.
auto try_vendor_cache = [&](const std::string& vendor_name) -> bool {
const std::string json_path = (dir / (vendor_name + ".json")).string();
const std::string ver_str = get_vendor_cache_key(json_path);
vendor_json_versions[vendor_name] = ver_str;
return try_load_and_apply_vendor_cache(vendor_name, ver_str);
};
// Sort: ORCA_FILAMENT_LIBRARY goes first, rest alphabetical.
std::sort(all_vendor_names.begin(), all_vendor_names.end(),
[](const std::string& a, const std::string& b) {
if (a == ORCA_FILAMENT_LIBRARY) return true;
if (b == ORCA_FILAMENT_LIBRARY) return false;
return a < b;
});
for (const auto& name : all_vendor_names) {
if (!try_vendor_cache(name)) {
cache_miss_vendors.insert(name);
all_from_cache = false;
}
}
if (all_from_cache && !all_vendor_names.empty()) {
update_system_maps();
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0).count();
BOOST_LOG_TRIVIAL(info) << "PresetBundle: system presets loaded from per-vendor cache in " << ms << " ms";
return {PresetsConfigSubstitutions{}, ""};
}
if (!cache_miss_vendors.empty())
BOOST_LOG_TRIVIAL(info) << "PresetBundle: " << cache_miss_vendors.size()
<< " vendor(s) need JSON parse: "
<< [&]{ std::string s; for (auto& v : cache_miss_vendors) s += v + " "; return s; }();
}
const auto json_load_t0 = std::chrono::steady_clock::now();
PresetsConfigSubstitutions substitutions;
std::string errors_cummulative;
bool first = true;
std::set<std::string> errored_vendors; // vendors whose JSON parse failed — skip their cache save
// first = true means no vendor has been loaded yet (from cache or JSON).
// false = at least one vendor was applied from the per-vendor cache above.
bool first = validation_mode || cache_miss_vendors.size() == vendor_json_versions.size();
std::vector<std::string> vendor_names;
// store all vendor names in vendor_names
for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) {
@@ -2207,6 +2292,9 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
std::vector<std::string> other_vendors;
other_vendors.reserve(vendor_names.size());
for (auto& vn : vendor_names) {
// Skip vendors already loaded from the per-vendor cache.
if (!validation_mode && !cache_miss_vendors.count(vn))
continue;
if (vn == ORCA_FILAMENT_LIBRARY)
orca_lib_vendor = vn;
else if (!(validation_mode && !vendor_to_validate.empty() && vn != vendor_to_validate))
@@ -2223,6 +2311,7 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
throw err;
errors_cummulative += err.what();
errors_cummulative += "\n";
errored_vendors.insert(orca_lib_vendor);
}
}
@@ -2259,6 +2348,7 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
throw std::runtime_error(parallel_errors[i]);
errors_cummulative += parallel_errors[i];
errors_cummulative += "\n";
errored_vendors.insert(other_vendors[i]);
continue;
}
if (!parallel_bundles[i])
@@ -2286,6 +2376,31 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
}
this->update_system_maps();
{
const auto json_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - json_load_t0).count();
BOOST_LOG_TRIVIAL(info) << "PresetBundle: system presets loaded from JSON in " << json_ms << " ms";
}
// Save per-vendor binary caches for vendors that parsed successfully.
// Vendors whose JSON produced a parse error are skipped individually so one
// bad vendor does not block caching of all others.
if (!validation_mode && !cache_miss_vendors.empty()) {
const auto save_t0 = std::chrono::steady_clock::now();
for (const auto& vendor_name : cache_miss_vendors) {
if (errored_vendors.count(vendor_name))
continue;
const bool is_orca_lib = (vendor_name == ORCA_FILAMENT_LIBRARY);
const std::string ver_str = vendor_json_versions.count(vendor_name)
? vendor_json_versions.at(vendor_name) : "";
write_vendor_cache(vendor_cache_user_path(vendor_name), vendor_name, ver_str, is_orca_lib);
}
const auto save_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - save_t0).count();
BOOST_LOG_TRIVIAL(info) << "PresetBundle: per-vendor caches saved in " << save_ms << " ms";
}
//BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, errors_cummulative %1%")%errors_cummulative;
return std::make_pair(std::move(substitutions), errors_cummulative);
@@ -5667,4 +5782,272 @@ bool BundleMetadata::save_to_json(const std::string& path) const
return false;
}
}
// ---- VendorCache implementation (PresetBundle methods) ------------------
// The VendorCacheHeader struct holds only metadata. Preset collections are
// serialized directly from/into PresetBundle's own prints/filaments/printers/…
// to avoid ever duplicating those vectors in memory.
namespace {
#pragma pack(push, 1)
struct CacheFileHeader {
uint32_t magic;
uint32_t version;
uint64_t data_size;
uint32_t crc32;
};
#pragma pack(pop)
static_assert(sizeof(CacheFileHeader) == 20, "CacheFileHeader must be 20 bytes");
} // anonymous namespace
// static
std::string PresetBundle::vendor_cache_user_path(const std::string& vendor_id)
{
return (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR / (vendor_id + ".cache"))
.make_preferred().string();
}
// static
std::string PresetBundle::vendor_cache_bundled_path(const std::string& vendor_id)
{
return (boost::filesystem::path(resources_dir()) / "profiles" / (vendor_id + ".cache"))
.make_preferred().string();
}
// static — raw file I/O + magic/version/CRC check, shared by all cache readers.
bool PresetBundle::read_cache_blob(const std::string& path, std::string& out_blob)
{
try {
boost::nowide::ifstream ifs(path, std::ios::binary);
if (!ifs.is_open())
return false;
CacheFileHeader fhdr;
if (!ifs.read(reinterpret_cast<char*>(&fhdr), sizeof(fhdr)))
return false;
if (fhdr.magic != VendorCacheHeader::CACHE_MAGIC || fhdr.version != VendorCacheHeader::CACHE_VERSION)
return false;
if (fhdr.data_size == 0 || fhdr.data_size > 512u * 1024u * 1024u)
return false;
out_blob.assign(fhdr.data_size, '\0');
if (!ifs.read(&out_blob[0], static_cast<std::streamsize>(fhdr.data_size)))
return false;
boost::crc_32_type crc;
crc.process_bytes(out_blob.data(), out_blob.size());
if (crc.checksum() != fhdr.crc32) {
BOOST_LOG_TRIVIAL(warning) << "VendorCache: CRC mismatch: " << path;
return false;
}
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "VendorCache: read failed (" << path << "): " << e.what();
return false;
}
}
// Serialize PresetBundle's preset collections for vendor_id directly to file.
// Uses cereal's size-tag protocol so the binary layout matches what a
// std::vector<Preset> archive would produce — no intermediate vector copy.
void PresetBundle::write_vendor_cache(const std::string& path,
const std::string& vendor_id,
const std::string& json_ver,
bool capture_filament_maps) const
{
std::ostringstream oss;
{
cereal::BinaryOutputArchive ar(oss);
VendorCacheHeader hdr;
hdr.cache_version = VendorCacheHeader::CACHE_VERSION;
hdr.config_options_count = static_cast<uint32_t>(print_config_def.options.size());
hdr.vendor_json_version = json_ver;
auto vp_it = vendors.find(vendor_id);
if (vp_it != vendors.end())
hdr.profile = vp_it->second;
ar(hdr);
// Write each collection filtered to vendor_id without copying into a temp vector.
auto write_col = [&](const PresetCollection& coll) {
std::vector<const Preset*> ptrs;
for (const Preset& p : coll())
if (p.is_system && p.vendor && p.vendor->id == vendor_id)
ptrs.push_back(&p);
ar(cereal::make_size_tag(static_cast<cereal::size_type>(ptrs.size())));
for (const Preset* p : ptrs)
ar(*p);
};
write_col(prints);
write_col(filaments);
write_col(printers);
write_col(sla_prints);
write_col(sla_materials);
if (capture_filament_maps) {
ar(m_config_maps, m_filament_id_maps);
} else {
const std::map<std::string, DynamicPrintConfig> empty_cm;
const std::map<std::string, std::string> empty_fi;
ar(empty_cm, empty_fi);
}
}
const std::string blob = oss.str();
boost::crc_32_type crc;
crc.process_bytes(blob.data(), blob.size());
try {
boost::filesystem::create_directories(boost::filesystem::path(path).parent_path());
boost::nowide::ofstream ofs(path, std::ios::binary | std::ios::trunc);
if (!ofs.is_open()) {
BOOST_LOG_TRIVIAL(warning) << "VendorCache: cannot open for writing: " << path;
return;
}
CacheFileHeader fhdr;
fhdr.magic = VendorCacheHeader::CACHE_MAGIC;
fhdr.version = VendorCacheHeader::CACHE_VERSION;
fhdr.data_size = static_cast<uint64_t>(blob.size());
fhdr.crc32 = crc.checksum();
ofs.write(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
ofs.write(blob.data(), static_cast<std::streamsize>(blob.size()));
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "VendorCache: write failed (" << path << "): " << e.what();
}
}
// Read a cache file and — if valid — apply its presets directly into PresetBundle's
// collections. Deserializes all 5 collections into temp vectors first so that
// PresetBundle is not modified if deserialization fails partway through.
bool PresetBundle::load_and_apply_vendor_cache(const std::string& path, const std::string& json_ver)
{
std::string blob;
if (!read_cache_blob(path, blob))
return false;
try {
std::istringstream iss(blob);
cereal::BinaryInputArchive ar(iss);
VendorCacheHeader hdr;
ar(hdr);
if (!hdr.is_valid(json_ver))
return false;
// Deserialize all collections before touching PresetBundle (rollback safety).
std::vector<Preset> col_print, col_filament, col_printer, col_sla_print, col_sla_material;
std::map<std::string, DynamicPrintConfig> config_maps;
std::map<std::string, std::string> filament_id_maps;
ar(col_print, col_filament, col_printer, col_sla_print, col_sla_material);
ar(config_maps, filament_id_maps);
// Full deserialization succeeded — apply to PresetBundle.
vendors.emplace(hdr.profile.id, hdr.profile);
const VendorProfile* vp = &vendors.at(hdr.profile.id);
auto apply_col = [&](std::vector<Preset>& cached, PresetCollection& coll, bool is_filaments) {
for (Preset& cp : cached) {
DynamicPrintConfig config = cp.config;
Preset& p = coll.load_preset(cp.file, cp.name, std::move(config), /*select=*/false, cp.version);
p.is_system = true;
p.is_visible = cp.is_visible;
p.alias = cp.alias;
p.renamed_from = cp.renamed_from;
p.filament_id = cp.filament_id;
p.setting_id = cp.setting_id;
p.description = cp.description;
p.m_from_orca_filament_lib = cp.m_from_orca_filament_lib;
p.m_excluded_from = cp.m_excluded_from;
p.vendor = vp;
if (is_filaments)
coll.set_printer_hold_alias(p.alias, p);
}
};
apply_col(col_print, prints, false);
apply_col(col_filament, filaments, true);
apply_col(col_printer, printers, false);
apply_col(col_sla_print, sla_prints, false);
apply_col(col_sla_material, sla_materials, false);
if (!config_maps.empty()) {
m_config_maps = std::move(config_maps);
m_filament_id_maps = std::move(filament_id_maps);
}
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "VendorCache: load failed (" << path << "): " << e.what();
return false;
}
}
bool PresetBundle::try_load_and_apply_vendor_cache(const std::string& vendor_id,
const std::string& json_ver)
{
if (load_and_apply_vendor_cache(vendor_cache_user_path(vendor_id), json_ver))
return true;
if (load_and_apply_vendor_cache(vendor_cache_bundled_path(vendor_id), json_ver)) {
// Promote bundled cache to user cache so future loads skip this path.
write_vendor_cache(vendor_cache_user_path(vendor_id), vendor_id, json_ver,
vendor_id == ORCA_FILAMENT_LIBRARY);
return true;
}
return false;
}
PresetBundle::VendorCacheStats PresetBundle::save_bundled_vendor_cache(
const std::string& vendor_id, const std::string& json_path,
bool capture_filament_maps, const std::string& output_path) const
{
const std::string json_ver = get_vendor_cache_key(json_path);
write_vendor_cache(output_path, vendor_id, json_ver, capture_filament_maps);
VendorCacheStats stats;
auto count_col = [&](const PresetCollection& coll) -> size_t {
size_t n = 0;
for (const Preset& p : coll())
if (p.is_system && p.vendor && p.vendor->id == vendor_id) ++n;
return n;
};
stats.print_presets = count_col(prints);
stats.filament_presets = count_col(filaments);
stats.printer_presets = count_col(printers);
// Verify: read back header and check validity.
if (std::string blob; read_cache_blob(output_path, blob)) {
try {
std::istringstream iss(blob);
cereal::BinaryInputArchive ar(iss);
VendorCacheHeader hdr; ar(hdr);
stats.ok = hdr.is_valid(json_ver);
} catch (...) {}
}
return stats;
}
// static
bool PresetBundle::load_vendor_cache_for_guide(const std::string& cache_path,
const std::string& json_ver,
VendorProfile& out_profile,
std::vector<Preset>& out_printer_presets,
std::vector<Preset>& out_filament_presets,
std::vector<Preset>& out_print_presets)
{
std::string blob;
if (!read_cache_blob(cache_path, blob))
return false;
try {
std::istringstream iss(blob);
cereal::BinaryInputArchive ar(iss);
VendorCacheHeader hdr; ar(hdr);
if (!hdr.is_valid(json_ver))
return false;
out_profile = std::move(hdr.profile);
// Must read in serialization order: print, filament, printer, sla_print, sla_material.
std::vector<Preset> col_sla_print, col_sla_material;
std::map<std::string, DynamicPrintConfig> cm;
std::map<std::string, std::string> fi;
ar(out_print_presets, out_filament_presets, out_printer_presets, col_sla_print, col_sla_material);
ar(cm, fi);
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "VendorCache: guide load failed (" << cache_path << "): " << e.what();
return false;
}
}
} // namespace Slic3r

View File

@@ -13,6 +13,7 @@
#include <boost/filesystem/path.hpp>
#include <unordered_set>
#define DEFAULT_USER_FOLDER_NAME "default"
#define BUNDLE_STRUCTURE_JSON_NAME "bundle_structure.json"
@@ -151,6 +152,39 @@ struct PresetBundleMetadata
class PresetBundle
{
public:
// ---- Per-vendor binary preset cache ------------------------------------
// One file per vendor:
// Bundled (CI-generated): resources/profiles/<vendor_id>.cache
// User (runtime): data_dir/system/<vendor_id>.cache
struct VendorCacheStats {
bool ok = false;
size_t print_presets = 0;
size_t filament_presets = 0;
size_t printer_presets = 0;
};
static std::string vendor_cache_user_path(const std::string& vendor_id);
static std::string vendor_cache_bundled_path(const std::string& vendor_id);
// Capture the given vendor from the loaded bundle, write to output_path,
// and verify the result. Used by generate_system_cache and tests.
// json_path is the vendor's .json profile file; its version/mtime is computed internally.
VendorCacheStats save_bundled_vendor_cache(const std::string& vendor_id,
const std::string& json_path,
bool capture_filament_maps,
const std::string& output_path) const;
// Load a vendor cache file and extract the data needed by the guide wizard.
// json_ver must match the cache's stored version key (use get_vendor_cache_key()).
// Returns false if the file is missing, stale, or corrupt.
static bool load_vendor_cache_for_guide(const std::string& cache_path,
const std::string& json_ver,
VendorProfile& out_profile,
std::vector<Preset>& out_printer_presets,
std::vector<Preset>& out_filament_presets,
std::vector<Preset>& out_print_presets);
static DynamicPrintConfig construct_full_config(Preset &in_printer_preset,
Preset &in_print_preset,
const DynamicPrintConfig &project_config,
@@ -484,6 +518,43 @@ public:
bool has_errors(bool check_duplicate_filament_subtypes = false) const;
private:
// ---- VendorCache — header-only metadata struct --------------------------
// Preset collections are NOT stored here; they are serialized directly from
// PresetBundle's own prints/filaments/printers/… on save, and applied directly
// into those collections on load. This avoids duplicating the preset vectors.
struct VendorCacheHeader {
static constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ"
static constexpr uint32_t CACHE_VERSION = 8;
uint32_t cache_version = CACHE_VERSION;
uint32_t config_options_count = 0;
std::string vendor_json_version;
VendorProfile profile;
template<class Archive>
void serialize(Archive& ar)
{
ar(cache_version, config_options_count, vendor_json_version, profile);
}
bool is_valid(const std::string& current_vendor_json_version) const
{
return cache_version == CACHE_VERSION
&& config_options_count == static_cast<uint32_t>(print_config_def.options.size())
&& vendor_json_version == current_vendor_json_version;
}
};
// Returns true and applies the cache if a valid file exists for vendor_id.
bool try_load_and_apply_vendor_cache(const std::string& vendor_id, const std::string& json_ver);
// Read a cache file, verify it, and apply its presets directly into PresetBundle's collections.
bool load_and_apply_vendor_cache(const std::string& path, const std::string& json_ver);
// Write PresetBundle's presets for vendor_id directly to a cache file — no intermediate copy.
void write_vendor_cache(const std::string& path, const std::string& vendor_id,
const std::string& json_ver, bool capture_filament_maps) const;
// Read raw blob bytes (file I/O + magic/CRC check) shared by all cache readers.
static bool read_cache_blob(const std::string& path, std::string& out_blob);
// Orca: validation only - flag any printer with two or more compatible
// filament presets sharing one filament_id (ambiguous AMS subtype match).
bool check_duplicate_filament_subtypes() const;

View File

@@ -2186,7 +2186,8 @@ namespace cereal {
archive(serialization_key_ordinal);
assert(serialization_key_ordinal > 0);
auto it = Slic3r::print_config_def.by_serialization_key_ordinal.find(serialization_key_ordinal);
assert(it != Slic3r::print_config_def.by_serialization_key_ordinal.end());
if (it == Slic3r::print_config_def.by_serialization_key_ordinal.end())
throw std::runtime_error("VendorCache: unknown serialization_key_ordinal " + std::to_string(serialization_key_ordinal) + " - cache is stale");
config.set_key_value(it->second->opt_key, it->second->load_option_from_archive(archive));
}
}

View File

@@ -190,6 +190,16 @@ public:
os << self.to_string();
return os;
}
// cereal: round-trip through the standard 3-part string (major.minor.patch).
// to_string() uses a BBS 4-part format that semver_parse() cannot read back.
template<class Archive>
std::string save_minimal(const Archive&) const { return to_string_sf(); }
template<class Archive>
void load_minimal(const Archive&, const std::string& s) {
if (auto v = Semver::parse(s)) *this = std::move(*v);
}
private:
semver_t ver;

View File

@@ -2,6 +2,7 @@
#include "ConfigWizard.hpp"
#include <boost/filesystem/operations.hpp>
#include <boost/nowide/fstream.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/iostreams/detail/select.hpp>
#include <boost/log/trivial.hpp>
@@ -9,6 +10,7 @@
#include "I18N.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/GUI/GUI_App.hpp"
@@ -41,7 +43,32 @@ using namespace nlohmann;
namespace Slic3r { namespace GUI {
json m_ProfileJson;
static std::string guide_json_cache_version_key(const boost::filesystem::path& rsrc_dir,
const boost::filesystem::path& user_dir)
{
std::vector<std::string> parts;
auto collect = [&](const boost::filesystem::path& dir) {
boost::system::error_code ec;
if (!boost::filesystem::exists(dir, ec)) return;
for (const auto& e : boost::filesystem::directory_iterator(dir, ec)) {
if (e.path().extension().string() != ".json") continue;
const std::string k = get_vendor_cache_key(e.path().string());
if (!k.empty())
parts.push_back(e.path().filename().string() + "=" + k);
}
};
collect(rsrc_dir);
if (user_dir != rsrc_dir) collect(user_dir);
std::sort(parts.begin(), parts.end());
std::string result;
for (const auto& p : parts) { result += p; result += ';'; }
return result;
}
static boost::filesystem::path guide_json_cache_path()
{
return boost::filesystem::path(data_dir()) / "guide_profile_cache.json";
}
static wxString update_custom_filaments()
{
@@ -191,11 +218,10 @@ GuideFrame::GuideFrame(GUI_App *pGUI, long style)
GuideFrame::~GuideFrame()
{
m_destroy = true;
if (m_load_task && m_load_task->joinable()) {
*m_cancel_token = true; // signal any queued CallAfter lambdas before join
if (m_load_task && m_load_task->joinable())
m_load_task->join();
delete m_load_task;
m_load_task = nullptr;
}
m_load_task.reset();
if (m_browser) {
delete m_browser;
m_browser = nullptr;
@@ -301,15 +327,85 @@ void GuideFrame::OnNavigationRequest(wxWebViewEvent &evt)
/**
* Callback invoked when a navigation request was accepted
*/
void GuideFrame::init_guide_paths()
{
m_ProfileJson = json::parse("{}");
m_ProfileJson["model"] = json::array();
m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array();
vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred();
rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred();
orca_bundle_rsrc = true;
if (boost::filesystem::exists(vendor_dir)) {
for (const auto& entry : boost::filesystem::directory_iterator(vendor_dir)) {
if (!boost::filesystem::is_directory(entry) &&
boost::iequals(entry.path().extension().string(), ".json") &&
!boost::iequals(entry.path().stem().string(), PresetBundle::ORCA_FILAMENT_LIBRARY)) {
orca_bundle_rsrc = false;
break;
}
}
}
auto lib_json = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json");
m_OrcaFilaLibPath = boost::filesystem::exists(vendor_dir / lib_json)
? (vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string()
: (rsrc_vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string();
}
void GuideFrame::on_profile_loaded()
{
// Must be called on the main thread.
SaveProfileData();
const std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents:\n" << strAll;
json res;
res["command"] = "userguide_profile_load_finish";
res["sequence_id"] = "10001";
RunScript(wxString::Format("HandleStudio(%s)", res.dump(-1, ' ', true)));
}
void GuideFrame::OnNavigationComplete(wxWebViewEvent &evt)
{
//wxLogMessage("%s", "Navigation complete; url='" + evt.GetURL() + "'");
if (!bFirstComplete) {
m_load_task = new boost::thread(boost::bind(&GuideFrame::LoadProfileData, this));
// boost::thread LoadProfileThread(boost::bind(&GuideFrame::LoadProfileData, this));
//LoadProfileThread.detach();
bFirstComplete = true;
try {
init_guide_paths();
if (BuildProfileDataFromPresetBundle()) {
// Persist so future opens that start before preset_bundle is ready
// can skip the slower loading paths. Capture by value so the
// thread is safe even if the dialog closes before it finishes.
boost::thread([data = m_ProfileJson,
rsrc = rsrc_vendor_dir,
user = vendor_dir] {
try {
json cache;
cache["version"] = guide_json_cache_version_key(rsrc, user);
if (cache["version"].get<std::string>().empty()) return;
json base = data;
for (auto& entry : base["model"]) entry["nozzle_selected"] = "";
cache["data"] = std::move(base);
boost::nowide::ofstream ofs(guide_json_cache_path().string());
ofs << cache.dump(-1, ' ', false, json::error_handler_t::ignore);
BOOST_LOG_TRIVIAL(info) << "GuideFrame: guide JSON cache saved";
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: failed to save guide JSON cache: " << e.what();
}
}).detach();
if (!m_destroy)
on_profile_loaded();
} else {
// Presets not yet in memory — delegate to background thread.
m_load_task = std::make_unique<boost::thread>(boost::bind(&GuideFrame::LoadProfileData, this));
}
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", init error: " << e.what();
m_load_task = std::make_unique<boost::thread>(boost::bind(&GuideFrame::LoadProfileData, this));
}
}
m_browser->Show();
@@ -1127,99 +1223,377 @@ int GuideFrame::GetFilamentInfo( std::string VendorDirectory, json & pFilaList,
return status;
}
int GuideFrame::LoadProfileData()
bool GuideFrame::TryLoadGuideJsonCache()
{
const auto path = guide_json_cache_path();
boost::system::error_code ec;
if (!boost::filesystem::exists(path, ec)) return false;
try {
boost::nowide::ifstream ifs(path.string());
json cache;
ifs >> cache;
if (!cache.contains("version") || !cache.contains("data")) return false;
const std::string expected = guide_json_cache_version_key(rsrc_vendor_dir, vendor_dir);
if (expected.empty() || cache["version"].get<std::string>() != expected) return false;
m_ProfileJson = cache["data"];
if (m_ProfileJson["machine"].empty()) return false;
BOOST_LOG_TRIVIAL(info) << "GuideFrame: loaded profile data from guide JSON cache ("
<< m_ProfileJson["model"].size() << " models, "
<< m_ProfileJson["machine"].size() << " machines, "
<< m_ProfileJson["filament"].size() << " filaments)";
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: guide JSON cache load failed: " << e.what();
return false;
}
}
void GuideFrame::SaveGuideJsonCache()
{
try {
m_ProfileJson = json::parse("{}");
json cache;
cache["version"] = guide_json_cache_version_key(rsrc_vendor_dir, vendor_dir);
if (cache["version"].get<std::string>().empty()) return;
json base = m_ProfileJson;
// Strip user-specific state — SaveProfileData() re-applies it from AppConfig.
for (auto& entry : base["model"])
entry["nozzle_selected"] = "";
cache["data"] = std::move(base);
boost::nowide::ofstream ofs(guide_json_cache_path().string());
ofs << cache.dump(-1, ' ', false, json::error_handler_t::ignore);
BOOST_LOG_TRIVIAL(info) << "GuideFrame: guide JSON cache saved";
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: failed to save guide JSON cache: " << e.what();
}
}
bool GuideFrame::BuildProfileDataFromPresetBundle()
{
PresetBundle* pb = wxGetApp().preset_bundle;
if (!pb || pb->vendors.empty())
return false;
try {
// Models from vendor profiles
for (const auto& [vendor_id, vp] : pb->vendors) {
for (const auto& model : vp.models) {
std::string nozzle_str;
for (const auto& v : model.variants) {
if (!nozzle_str.empty()) nozzle_str += ";";
nozzle_str += v.name;
}
std::string materials_str;
for (const auto& m : model.default_materials) {
if (!materials_str.empty()) materials_str += ";";
materials_str += m;
}
boost::filesystem::path cover_path =
(boost::filesystem::path(resources_dir()) / "profiles" / vendor_id / (model.id + "_cover.png"))
.make_preferred();
if (!boost::filesystem::exists(cover_path))
cover_path =
(boost::filesystem::path(resources_dir()) / "web/image/printer" / (model.id + "_cover.png"))
.make_preferred();
json entry;
entry["model"] = model.id;
entry["name"] = model.name;
entry["vendor"] = vendor_id;
entry["nozzle_diameter"] = nozzle_str;
entry["materials"] = materials_str;
entry["cover"] = cover_path.string();
entry["nozzle_selected"] = "";
entry["sub_path"] = "";
m_ProfileJson["model"].push_back(entry);
}
}
// Machine map: preset name -> {model, nozzle variant}
for (const Preset& p : pb->printers()) {
if (!p.is_system) continue;
const auto* printer_model = p.config.option<ConfigOptionString>("printer_model");
const auto* printer_variant = p.config.option<ConfigOptionString>("printer_variant");
if (!printer_model || printer_model->value.empty() || !printer_variant) continue;
json mach;
mach["model"] = printer_model->value;
mach["nozzle"] = printer_variant->value;
m_ProfileJson["machine"][p.name] = mach;
}
// Filament map from system filament presets (vendor/type already resolved in config)
for (const Preset& p : pb->filaments()) {
if (!p.is_system) continue;
const auto* fila_vendor = p.config.option<ConfigOptionStrings>("filament_vendor");
const auto* fila_type = p.config.option<ConfigOptionStrings>("filament_type");
const auto* compat_printers = p.config.option<ConfigOptionStrings>("compatible_printers");
std::string vendor = (fila_vendor && !fila_vendor->values.empty()) ? fila_vendor->values[0] : "";
std::string type = (fila_type && !fila_type->values.empty()) ? fila_type->values[0] : "";
std::string model_list;
if (compat_printers) {
for (const std::string& pname : compat_printers->values) {
if (m_ProfileJson["machine"].contains(pname)) {
std::string m = m_ProfileJson["machine"][pname]["model"];
std::string n = m_ProfileJson["machine"][pname]["nozzle"];
model_list += "[" + m + "++" + n + "]";
}
}
}
json ff;
ff["name"] = p.name;
ff["sub_path"] = p.file;
ff["vendor"] = vendor;
ff["type"] = type;
ff["models"] = model_list;
ff["selected"] = 0;
m_ProfileJson["filament"][p.name] = ff;
}
// Process list from visible system print presets
for (const Preset& p : pb->prints()) {
if (!p.is_system || !p.is_visible) continue;
json entry;
entry["name"] = p.name;
entry["sub_path"] = p.file;
m_ProfileJson["process"].push_back(entry);
}
// If rsrc_vendor_dir has vendor JSONs not covered by the current bundle, the
// bundle is incomplete (e.g. dev env where data_dir/system only has
// OrcaFilamentLibrary+Custom). Fall back so LoadProfileFamily reads both dirs.
try {
for (const auto& e : boost::filesystem::directory_iterator(rsrc_vendor_dir)) {
if (e.path().extension().string() != ".json") continue;
const std::string stem = e.path().stem().string();
if (pb->vendors.find(stem) == pb->vendors.end()) {
BOOST_LOG_TRIVIAL(info) << "GuideFrame: vendor '" << stem
<< "' in resources but not in preset_bundle — falling back to JSON loading";
m_ProfileJson["model"] = json::array();
m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array();
return false;
}
}
} catch (const std::exception&) {}
BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data from preset_bundle ("
<< m_ProfileJson["model"].size() << " models, "
<< m_ProfileJson["machine"].size() << " machines, "
<< m_ProfileJson["filament"].size() << " filaments)";
return !m_ProfileJson["machine"].empty();
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileDataFromPresetBundle failed: " << e.what()
<< " — falling back to JSON loading";
m_ProfileJson["model"] = json::array();
m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array();
return false;
}
}
vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred();
rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred();
// Builds guide profile JSON from the per-vendor bundled caches
// (resources/profiles/<vendor>.cache, generated by CI).
// This avoids the 90-second LoadProfileFamily fallback on first launch.
bool GuideFrame::BuildProfileDataFromBundledCache()
{
// Enumerate per-vendor .cache files in resources/profiles/.
std::vector<boost::filesystem::path> cache_files;
try {
for (const auto& e : boost::filesystem::directory_iterator(rsrc_vendor_dir)) {
if (e.path().extension() == ".cache")
cache_files.push_back(e.path());
}
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileDataFromBundledCache: cannot scan " << rsrc_vendor_dir << ": " << ex.what();
return false;
}
if (cache_files.empty())
return false;
// Orca: add custom as default
// Orca: add json logic for vendor bundle
orca_bundle_rsrc = true;
try {
for (const auto& cache_path : cache_files) {
const std::string vendor_id = cache_path.stem().string();
// Validate against the version in the corresponding vendor JSON.
const boost::filesystem::path json_path = rsrc_vendor_dir / (vendor_id + ".json");
const std::string ver_str = get_vendor_cache_key(json_path.string());
// search if there exists a .json file in vendor_dir folder, if exists, set orca_bundle_rsrc to false
for (const auto& entry : boost::filesystem::directory_iterator(vendor_dir)) {
if (!boost::filesystem::is_directory(entry) && boost::iequals(entry.path().extension().string(), ".json") && !boost::iequals(entry.path().stem().string(), PresetBundle::ORCA_FILAMENT_LIBRARY)) {
orca_bundle_rsrc = false;
break;
VendorProfile vc_profile;
std::vector<Preset> vc_printer_presets;
std::vector<Preset> vc_filament_presets;
std::vector<Preset> vc_print_presets;
if (!PresetBundle::load_vendor_cache_for_guide(cache_path.string(), ver_str,
vc_profile, vc_printer_presets,
vc_filament_presets, vc_print_presets))
continue;
// Models from this vendor's cached profile
for (const auto& cm : vc_profile.models) {
std::string nozzle_str;
for (const auto& v : cm.variants) {
if (!nozzle_str.empty()) nozzle_str += ";";
nozzle_str += v.name;
}
std::string materials_str;
for (const auto& m : cm.default_materials) {
if (!materials_str.empty()) materials_str += ";";
materials_str += m;
}
boost::filesystem::path cover_path =
(boost::filesystem::path(resources_dir()) / "profiles" / vc_profile.id / (cm.id + "_cover.png"))
.make_preferred();
if (!boost::filesystem::exists(cover_path))
cover_path =
(boost::filesystem::path(resources_dir()) / "web/image/printer" / (cm.id + "_cover.png"))
.make_preferred();
json entry;
entry["model"] = cm.id;
entry["name"] = cm.name;
entry["vendor"] = vc_profile.id;
entry["nozzle_diameter"] = nozzle_str;
entry["materials"] = materials_str;
entry["cover"] = cover_path.string();
entry["nozzle_selected"] = "";
entry["sub_path"] = "";
m_ProfileJson["model"].push_back(entry);
}
// Machines from cached printer presets
for (const auto& cp : vc_printer_presets) {
const auto* pm = cp.config.option<ConfigOptionString>("printer_model");
const auto* pv = cp.config.option<ConfigOptionString>("printer_variant");
if (!pm || pm->value.empty() || !pv) continue;
json mach;
mach["model"] = pm->value;
mach["nozzle"] = pv->value;
m_ProfileJson["machine"][cp.name] = mach;
}
// Filaments from cached filament presets
for (const auto& cp : vc_filament_presets) {
const auto* fv = cp.config.option<ConfigOptionStrings>("filament_vendor");
const auto* ft = cp.config.option<ConfigOptionStrings>("filament_type");
const auto* compat = cp.config.option<ConfigOptionStrings>("compatible_printers");
std::string vendor = (fv && !fv->values.empty()) ? fv->values[0] : "";
std::string type = (ft && !ft->values.empty()) ? ft->values[0] : "";
std::string model_list;
if (compat) {
for (const std::string& pname : compat->values) {
if (m_ProfileJson["machine"].contains(pname)) {
std::string m = m_ProfileJson["machine"][pname]["model"];
std::string n = m_ProfileJson["machine"][pname]["nozzle"];
model_list += "[" + m + "++" + n + "]";
}
}
}
json ff;
ff["name"] = cp.name;
ff["sub_path"] = cp.file;
ff["vendor"] = vendor;
ff["type"] = type;
ff["models"] = model_list;
ff["selected"] = 0;
m_ProfileJson["filament"][cp.name] = ff;
}
// Process from cached print presets
for (const auto& cp : vc_print_presets) {
if (!cp.is_visible) continue;
json entry;
entry["name"] = cp.name;
entry["sub_path"] = cp.file;
m_ProfileJson["process"].push_back(entry);
}
}
// load the default filament library first
std::set<std::string> loaded_vendors;
auto filament_library_name = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json");
if (boost::filesystem::exists(vendor_dir / filament_library_name)) {
m_OrcaFilaLibPath = (vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string();
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (vendor_dir / filament_library_name).string());
} else {
m_OrcaFilaLibPath = (rsrc_vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string();
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (rsrc_vendor_dir / filament_library_name).string());
}
loaded_vendors.insert(PresetBundle::ORCA_FILAMENT_LIBRARY);
BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data from bundled per-vendor caches ("
<< m_ProfileJson["model"].size() << " models, "
<< m_ProfileJson["machine"].size() << " machines, "
<< m_ProfileJson["filament"].size() << " filaments)";
return !m_ProfileJson["machine"].empty();
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileDataFromBundledCache failed: " << ex.what();
m_ProfileJson["model"] = json::array();
m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array();
return false;
}
}
//load custom bundle from user data path
boost::filesystem::directory_iterator endIter;
for (boost::filesystem::directory_iterator iter(vendor_dir); iter != endIter; iter++) {
if (!boost::filesystem::is_directory(*iter)) {
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
strVendor = strVendor.AfterLast('\\');
strVendor = strVendor.AfterLast('/');
int GuideFrame::LoadProfileData()
{
// Background thread: the fast path in OnNavigationComplete failed (presets not yet loaded).
// Loading order (fastest to slowest):
// 1. Guide JSON cache (data_dir/guide_profile_cache.json, sub-second)
// 2. Bundled per-vendor binary caches (CI-generated, ~1-2s)
// 3. Read all vendor JSONs (~90s)
// After paths 2 or 3 the guide JSON cache is written so next open uses path 1.
try {
if (!TryLoadGuideJsonCache()) {
if (!BuildProfileDataFromBundledCache()) {
// Last resort — read all vendor JSONs (~90s)
std::set<std::string> loaded_vendors;
auto filament_library_name = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json");
if (boost::filesystem::exists(vendor_dir / filament_library_name))
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (vendor_dir / filament_library_name).string());
else
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (rsrc_vendor_dir / filament_library_name).string());
loaded_vendors.insert(PresetBundle::ORCA_FILAMENT_LIBRARY);
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
if(strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
continue;
boost::filesystem::directory_iterator endIter;
for (boost::filesystem::directory_iterator iter(vendor_dir); iter != endIter; iter++) {
if (!boost::filesystem::is_directory(*iter)) {
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
strVendor = strVendor.AfterLast('\\');
strVendor = strVendor.AfterLast('/');
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
continue;
LoadProfileFamily(w2s(strVendor), iter->path().string());
loaded_vendors.insert(w2s(strVendor));
}
if (m_destroy) return 0;
}
LoadProfileFamily(w2s(strVendor), iter->path().string());
loaded_vendors.insert(w2s(strVendor));
boost::filesystem::directory_iterator others_endIter;
for (boost::filesystem::directory_iterator iter(rsrc_vendor_dir); iter != others_endIter; iter++) {
if (!boost::filesystem::is_directory(*iter)) {
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
strVendor = strVendor.AfterLast('\\');
strVendor = strVendor.AfterLast('/');
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
continue;
LoadProfileFamily(w2s(strVendor), iter->path().string());
loaded_vendors.insert(w2s(strVendor));
}
if (m_destroy) return 0;
}
}
if (m_destroy)
return 0;
// Persist the result so subsequent opens skip both the bundled cache and
// the slow JSON loading path entirely.
SaveGuideJsonCache();
}
boost::filesystem::directory_iterator others_endIter;
for (boost::filesystem::directory_iterator iter(rsrc_vendor_dir); iter != others_endIter; iter++) {
if (!boost::filesystem::is_directory(*iter)) {
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
strVendor = strVendor.AfterLast('\\');
strVendor = strVendor.AfterLast('/');
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
continue;
LoadProfileFamily(w2s(strVendor), iter->path().string());
loaded_vendors.insert(w2s(strVendor));
}
if (m_destroy)
return 0;
}
wxGetApp().CallAfter([this] {
if (!m_destroy) {
//sync to appconfig first to populate current selections
SaveProfileData();
//sync to web after selections are populated
std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents: " << std::endl << strAll;
json m_Res = json::object();
m_Res["command"] = "userguide_profile_load_finish";
m_Res["sequence_id"] = "10001";
wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', true));
RunScript(strJS);
}
// Capture the cancel token by value (shared_ptr) so the lambda doesn't
// touch `this` if GuideFrame is destroyed before the event fires.
auto tok = m_cancel_token;
wxGetApp().CallAfter([this, tok] {
if (!*tok)
on_profile_loaded();
});
} catch (std::exception& e) {
// wxLogMessage("GUIDE: load_profile_error %s ", e.what());
// wxMessageBox(e.what(), "", MB_OK);
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", error: " << e.what() << std::endl;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", error: " << e.what();
}
filament_info_cache.clear();

View File

@@ -30,10 +30,14 @@
#include "libslic3r/PresetBundle.hpp"
#include "slic3r/Utils/PresetUpdater.hpp"
#include <atomic>
#include <memory>
#include <unordered_map>
#include <nlohmann/json.hpp>
#include <boost/thread.hpp>
namespace Slic3r { namespace GUI {
class GuideFrame : public DPIDialog
@@ -78,6 +82,12 @@ public:
int LoadProfileData();
int SaveProfileData();
int LoadProfileFamily(std::string strVendor, std::string strFilePath);
void init_guide_paths();
void on_profile_loaded();
bool BuildProfileDataFromPresetBundle();
bool BuildProfileDataFromBundledCache();
bool TryLoadGuideJsonCache();
void SaveGuideJsonCache();
int SaveProfile();
int GetFilamentInfo( std::string VendorDirectory,json & pFilaList, std::string filepath, std::string &sVendor, std::string &sType);
@@ -112,8 +122,11 @@ private:
//First Load
bool bFirstComplete{false};
bool m_destroy{false};
boost::thread* m_load_task{ nullptr };
std::atomic<bool> m_destroy{false};
// Shared cancel token captured by CallAfter lambdas so they don't touch
// `this` after the destructor has run and the object is freed.
std::shared_ptr<std::atomic<bool>> m_cancel_token{std::make_shared<std::atomic<bool>>(false)};
std::unique_ptr<boost::thread> m_load_task;
// User Config
bool PrivacyUse;
@@ -123,6 +136,7 @@ private:
bool InstallNetplugin;
bool network_plugin_ready {false};
json m_ProfileJson;
json m_OrcaFilaList;
std::string m_OrcaFilaLibPath;

View File

@@ -14,6 +14,7 @@ add_executable(${_TEST_NAME}_tests
test_config.cpp
test_preset_bundle_loading.cpp
test_preset_setting_id.cpp
test_vendor_cache.cpp
test_elephant_foot_compensation.cpp
test_geometry.cpp
test_placeholder_parser.cpp

View File

@@ -0,0 +1,509 @@
#include <catch2/catch_all.hpp>
#include <boost/filesystem.hpp>
#include <boost/crc.hpp>
#include <fstream>
#include <set>
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PrintConfig.hpp"
using namespace Slic3r;
namespace fs = boost::filesystem;
namespace {
struct TempDir {
fs::path path;
TempDir() {
path = fs::temp_directory_path() / fs::unique_path("orca-cache-test-%%%%-%%%%");
fs::create_directories(path);
}
~TempDir() { boost::system::error_code ec; fs::remove_all(path, ec); }
};
// Write a minimal vendor JSON with a version field so get_vendor_cache_key() returns
// a deterministic Semver string rather than an mtime-based key.
std::string write_vendor_json(const fs::path& dir, const std::string& vendor_id,
const std::string& version = "1.0.0")
{
const fs::path p = dir / (vendor_id + ".json");
std::ofstream f(p.string());
f << R"({"version":")" << version << R"(","name":")" << vendor_id << R"("})";
return p.string();
}
// Write a vendor JSON without a "version" field — get_vendor_cache_key() will return
// "mtime:<timestamp>" instead of a Semver string.
std::string write_versionless_vendor_json(const fs::path& dir, const std::string& vendor_id)
{
const fs::path p = dir / (vendor_id + ".json");
std::ofstream f(p.string());
f << R"({"name":")" << vendor_id << R"("})";
return p.string();
}
// Binary-patch the config_options_count field in a cache file (blob offset 4) and
// recompute the CRC so the file passes the magic/size/CRC checks but fails is_valid().
// File layout: [20-byte CacheFileHeader][blob]; in blob: uint32 cache_version @ 0, uint32 options_count @ 4.
void patch_cache_options_count(const std::string& path, uint32_t wrong_count)
{
std::ifstream in(path, std::ios::binary);
std::vector<char> data(std::istreambuf_iterator<char>(in), {});
in.close();
if (data.size() < 28) return; // 20-byte header + 4 (cache_version) + 4 (options_count)
std::memcpy(&data[24], &wrong_count, 4); // file[24..27] = blob[4..7] = options_count
// Recompute CRC over the modified blob (everything after the 20-byte file header).
boost::crc_32_type crc;
crc.process_bytes(&data[20], data.size() - 20);
const uint32_t new_crc = crc.checksum();
std::memcpy(&data[16], &new_crc, 4); // file[16..19] = crc32 field in CacheFileHeader
std::ofstream out(path, std::ios::binary | std::ios::trunc);
out.write(data.data(), static_cast<std::streamsize>(data.size()));
}
void add_vendor(PresetBundle& bundle, const std::string& vendor_id)
{
VendorProfile vp(vendor_id);
vp.name = vendor_id + " Corp";
vp.config_version = Semver(1, 0, 0);
bundle.vendors.emplace(vendor_id, vp);
}
Preset& add_system_preset(PresetCollection& coll, const std::string& name,
const VendorProfile* vp)
{
Preset& p = coll.load_preset("", name, DynamicPrintConfig(coll.default_preset().config), false);
p.is_system = true;
p.vendor = vp;
return p;
}
} // namespace
TEST_CASE("VendorCache: save and load via guide API", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
const std::string json_path = write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / (vid + ".cache");
PresetBundle src;
add_vendor(src, vid);
const VendorProfile* vp = &src.vendors.at(vid);
Preset& fp = add_system_preset(src.filaments, vid + " PLA @0.4", vp);
fp.alias = "Acme PLA";
fp.filament_id = "GFL_acme_pla";
add_system_preset(src.printers, vid + " Printer 0.4", vp);
const auto stats = src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
REQUIRE(stats.ok);
CHECK(stats.filament_presets == 1);
CHECK(stats.printer_presets == 1);
CHECK(stats.print_presets == 0);
VendorProfile out_profile;
std::vector<Preset> out_printers, out_filaments, out_prints;
REQUIRE(PresetBundle::load_vendor_cache_for_guide(
cache.string(), get_vendor_cache_key(json_path),
out_profile, out_printers, out_filaments, out_prints));
CHECK(out_profile.id == vid);
REQUIRE(out_filaments.size() == 1);
CHECK(out_filaments[0].name == vid + " PLA @0.4");
CHECK(out_filaments[0].alias == "Acme PLA");
CHECK(out_filaments[0].filament_id == "GFL_acme_pla");
REQUIRE(out_printers.size() == 1);
CHECK(out_printers[0].name == vid + " Printer 0.4");
}
TEST_CASE("VendorCache: stale json_ver is rejected", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
const std::string json_path = write_vendor_json(tmp.path, vid, "1.0.0");
const fs::path cache = tmp.path / (vid + ".cache");
PresetBundle src;
add_vendor(src, vid);
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
VendorProfile p; std::vector<Preset> pr, fi, pp;
CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide(cache.string(), "2.0.0", p, pr, fi, pp));
}
TEST_CASE("VendorCache: missing file returns false", "[VendorCache]")
{
TempDir tmp;
VendorProfile p; std::vector<Preset> pr, fi, pp;
CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide(
(tmp.path / "nonexistent.cache").string(), "1.0.0", p, pr, fi, pp));
}
TEST_CASE("VendorCache: corrupt data is rejected by CRC check", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
const std::string json_path = write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / (vid + ".cache");
PresetBundle src;
add_vendor(src, vid);
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
// Flip a byte in the data region (after the 20-byte CacheFileHeader).
{
std::fstream f(cache.string(), std::ios::in | std::ios::out | std::ios::binary);
f.seekp(30);
char b; f.read(&b, 1);
f.seekp(30);
b ^= 0xFF;
f.write(&b, 1);
}
VendorProfile p; std::vector<Preset> pr, fi, pp;
CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide(
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
}
TEST_CASE("VendorCache: multiple vendors do not bleed into each other", "[VendorCache]")
{
TempDir tmp;
const std::string vid1 = "VendorA";
const std::string vid2 = "VendorB";
PresetBundle src;
const std::string json1 = write_vendor_json(tmp.path, vid1);
const std::string json2 = write_vendor_json(tmp.path, vid2);
for (const auto& vid : {vid1, vid2}) {
add_vendor(src, vid);
const VendorProfile* vp = &src.vendors.at(vid);
add_system_preset(src.filaments, vid + " PLA", vp);
add_system_preset(src.printers, vid + " Printer", vp);
}
src.save_bundled_vendor_cache(vid1, json1, false, (tmp.path / (vid1 + ".cache")).string());
src.save_bundled_vendor_cache(vid2, json2, false, (tmp.path / (vid2 + ".cache")).string());
for (const auto& [vid, jpath] : std::vector<std::pair<std::string, std::string>>{{vid1, json1}, {vid2, json2}}) {
VendorProfile p; std::vector<Preset> pr, fi, pp;
REQUIRE(PresetBundle::load_vendor_cache_for_guide(
(tmp.path / (vid + ".cache")).string(), get_vendor_cache_key(jpath),
p, pr, fi, pp));
REQUIRE(fi.size() == 1);
CHECK(fi[0].name == vid + " PLA");
REQUIRE(pr.size() == 1);
CHECK(pr[0].name == vid + " Printer");
}
}
TEST_CASE("VendorCache: vendor profile fields are preserved", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
const std::string json_path = write_vendor_json(tmp.path, vid, "2.5.1");
const fs::path cache = tmp.path / (vid + ".cache");
PresetBundle src;
VendorProfile vp(vid);
vp.name = "Acme Corporation";
vp.config_version = Semver(2, 5, 1);
VendorProfile::PrinterModel model;
model.id = "AcmePro";
model.name = "Acme Pro";
VendorProfile::PrinterVariant v0_4; v0_4.name = "0.4";
model.variants.push_back(v0_4);
vp.models.push_back(model);
src.vendors.emplace(vid, vp);
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
VendorProfile out_p; std::vector<Preset> pr, fi, pp;
REQUIRE(PresetBundle::load_vendor_cache_for_guide(
cache.string(), get_vendor_cache_key(json_path), out_p, pr, fi, pp));
CHECK(out_p.id == vid);
CHECK(out_p.name == "Acme Corporation");
REQUIRE(out_p.models.size() == 1);
CHECK(out_p.models[0].id == "AcmePro");
CHECK(out_p.models[0].name == "Acme Pro");
REQUIRE(out_p.models[0].variants.size() == 1);
CHECK(out_p.models[0].variants[0].name == "0.4");
}
TEST_CASE("VendorCache: config option values are preserved", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
const std::string json_path = write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / (vid + ".cache");
PresetBundle src;
add_vendor(src, vid);
const VendorProfile* vp = &src.vendors.at(vid);
Preset& fp = add_system_preset(src.filaments, vid + " PETG @0.4", vp);
fp.config.set_key_value("filament_type", new ConfigOptionStrings({"PETG"}));
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
VendorProfile p; std::vector<Preset> pr, fi, pp;
REQUIRE(PresetBundle::load_vendor_cache_for_guide(
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
REQUIRE(fi.size() == 1);
const auto* ft = fi[0].config.option<ConfigOptionStrings>("filament_type");
REQUIRE(ft != nullptr);
REQUIRE(ft->values.size() >= 1);
CHECK(ft->values[0] == "PETG");
}
TEST_CASE("VendorCache: multiple presets per collection round-trip", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
const std::string json_path = write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / (vid + ".cache");
PresetBundle src;
add_vendor(src, vid);
const VendorProfile* vp = &src.vendors.at(vid);
const std::vector<std::string> fi_names = {vid + " PLA", vid + " PETG", vid + " ABS"};
const std::vector<std::string> pr_names = {vid + " Printer 0.4", vid + " Printer 0.6"};
for (const auto& n : fi_names) add_system_preset(src.filaments, n, vp);
for (const auto& n : pr_names) add_system_preset(src.printers, n, vp);
const auto stats = src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
REQUIRE(stats.ok);
CHECK(stats.filament_presets == 3);
CHECK(stats.printer_presets == 2);
VendorProfile p; std::vector<Preset> pr, fi, pp;
REQUIRE(PresetBundle::load_vendor_cache_for_guide(
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
REQUIRE(fi.size() == 3);
REQUIRE(pr.size() == 2);
std::set<std::string> fi_got, pr_got;
for (const auto& x : fi) fi_got.insert(x.name);
for (const auto& x : pr) pr_got.insert(x.name);
for (const auto& n : fi_names) CHECK(fi_got.count(n) == 1);
for (const auto& n : pr_names) CHECK(pr_got.count(n) == 1);
}
TEST_CASE("VendorCache: truncated file is rejected", "[VendorCache]")
{
TempDir tmp;
const fs::path cache = tmp.path / "truncated.cache";
// 3 bytes — shorter than the 20-byte CacheFileHeader
{
std::ofstream f(cache.string(), std::ios::binary);
const char data[] = {0x4F, 0x52, 0x43};
f.write(data, sizeof(data));
}
VendorProfile p; std::vector<Preset> pr, fi, pp;
CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide(
cache.string(), "1.0.0", p, pr, fi, pp));
}
TEST_CASE("VendorCache: wrong magic is rejected", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
const std::string json_path = write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / (vid + ".cache");
PresetBundle src;
add_vendor(src, vid);
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
// Overwrite first 4 bytes (magic field) with a wrong value; rest of file stays valid.
{
std::fstream f(cache.string(), std::ios::in | std::ios::out | std::ios::binary);
const uint32_t bad = 0xDEADBEEFu;
f.write(reinterpret_cast<const char*>(&bad), sizeof(bad));
}
VendorProfile p; std::vector<Preset> pr, fi, pp;
CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide(
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
}
TEST_CASE("VendorCache: vendor with no presets saves and loads cleanly", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
const std::string json_path = write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / (vid + ".cache");
PresetBundle src;
VendorProfile vp(vid);
vp.name = "Acme Corporation";
vp.config_version = Semver(1, 0, 0);
src.vendors.emplace(vid, vp);
const auto stats = src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
REQUIRE(stats.ok);
CHECK(stats.print_presets == 0);
CHECK(stats.filament_presets == 0);
CHECK(stats.printer_presets == 0);
VendorProfile p; std::vector<Preset> pr, fi, pp;
REQUIRE(PresetBundle::load_vendor_cache_for_guide(
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
CHECK(p.id == vid);
CHECK(p.name == "Acme Corporation");
CHECK(fi.empty());
CHECK(pr.empty());
CHECK(pp.empty());
}
TEST_CASE("VendorCache: all Preset metadata fields are preserved", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
const std::string json_path = write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / (vid + ".cache");
PresetBundle src;
add_vendor(src, vid);
const VendorProfile* vp = &src.vendors.at(vid);
Preset& fp = add_system_preset(src.filaments, vid + " PLA @0.4", vp);
fp.setting_id = "sid-test-001";
fp.description = "A test filament preset";
fp.bundle_id = "bundle-xyz";
fp.user_id = "user-abc";
fp.base_id = "base-123";
fp.sync_info = "update";
fp.updated_time = 1700000000LL;
fp.key_values = {{"color", "red"}, {"diameter", "1.75"}};
fp.ini_str = "[filament]\nnozzle_temperature = 230\n";
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
VendorProfile p; std::vector<Preset> pr, fi, pp;
REQUIRE(PresetBundle::load_vendor_cache_for_guide(
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
REQUIRE(fi.size() == 1);
const Preset& out = fi[0];
CHECK(out.setting_id == "sid-test-001");
CHECK(out.description == "A test filament preset");
CHECK(out.bundle_id == "bundle-xyz");
CHECK(out.user_id == "user-abc");
CHECK(out.base_id == "base-123");
CHECK(out.sync_info == "update");
CHECK(out.updated_time == 1700000000LL);
REQUIRE(out.key_values.count("color") == 1);
CHECK(out.key_values.at("color") == "red");
REQUIRE(out.key_values.count("diameter") == 1);
CHECK(out.key_values.at("diameter") == "1.75");
CHECK(out.ini_str == "[filament]\nnozzle_temperature = 230\n");
}
// TC09 — versionless JSON uses mtime as the cache key; same mtime → cache valid.
// TC10 — after the JSON file is touched (mtime changes), the old key no longer matches.
TEST_CASE("VendorCache: versionless vendor uses mtime key", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
const std::string json_path = write_versionless_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / (vid + ".cache");
PresetBundle src;
add_vendor(src, vid);
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
const std::string key_before = get_vendor_cache_key(json_path);
REQUIRE_FALSE(key_before.empty());
// Must be mtime-based, not a Semver.
REQUIRE(key_before.substr(0, 6) == "mtime:");
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
SECTION("same mtime → cache is valid") {
VendorProfile p; std::vector<Preset> pr, fi, pp;
CHECK(PresetBundle::load_vendor_cache_for_guide(
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
CHECK(fi.size() == 1);
}
SECTION("touched file changes mtime → cache is invalid") {
// Advance mtime by 2 seconds so the key definitely differs.
const std::time_t old_mtime = fs::last_write_time(json_path);
fs::last_write_time(json_path, old_mtime + 2);
const std::string key_after = get_vendor_cache_key(json_path);
REQUIRE(key_after != key_before);
VendorProfile p; std::vector<Preset> pr, fi, pp;
CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide(
cache.string(), key_after, p, pr, fi, pp));
}
}
// TC07 — config_options_count in the cache header must equal the live print_config_def
// option count. A patched value (1) is rejected by is_valid() even if the CRC is correct.
TEST_CASE("VendorCache: config_options_count mismatch is rejected", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
const std::string json_path = write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / (vid + ".cache");
PresetBundle src;
add_vendor(src, vid);
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
// Patch config_options_count to 1 and fix CRC — file is structurally valid but semantically stale.
patch_cache_options_count(cache.string(), 1u);
VendorProfile p; std::vector<Preset> pr, fi, pp;
CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide(
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
}
// TC12 variant — header is intact but the blob is cut short; the blob read fails.
TEST_CASE("VendorCache: mid-blob truncation is rejected", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
const std::string json_path = write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / (vid + ".cache");
PresetBundle src;
add_vendor(src, vid);
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
src.save_bundled_vendor_cache(vid, json_path, false, cache.string());
// Keep the full 20-byte CacheFileHeader intact; truncate the blob to 10 bytes.
// data_size in the header still says the full blob length, so the read will fail.
{
std::ifstream in(cache.string(), std::ios::binary);
std::vector<char> buf(30); // 20-byte header + 10 bytes of blob
in.read(buf.data(), 30);
in.close();
std::ofstream out(cache.string(), std::ios::binary | std::ios::trunc);
out.write(buf.data(), 30);
}
VendorProfile p; std::vector<Preset> pr, fi, pp;
CHECK_FALSE(PresetBundle::load_vendor_cache_for_guide(
cache.string(), get_vendor_cache_key(json_path), p, pr, fi, pp));
}