diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 9b67d7ab50..80aa365bec 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -344,6 +344,8 @@ set(lisbslic3r_sources Polyline.hpp PresetBundle.cpp PresetBundle.hpp + PresetBundleCache.cpp + PresetBundleCache.hpp Preset.cpp Preset.hpp PrincipalComponents2D.cpp diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 9fbfe906c3..6f165b7c55 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -4,6 +4,7 @@ #include "Exception.hpp" #include "Preset.hpp" #include "PresetBundle.hpp" +#include "PresetBundleCache.hpp" #include "AppConfig.hpp" #ifdef _MSC_VER @@ -707,6 +708,9 @@ void Preset::save(DynamicPrintConfig* parent_config) idx_file.replace_extension(".info"); this->save_info(idx_file.string()); } + + // Update the per-file binary cache so the next startup skips JSON parsing. + PresetBundleCache::write_file_cache(*this); } void Preset::reload(Preset const &parent) diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 1ace4db685..af5742e2fd 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -1,7 +1,9 @@ #include +#include #include #include "PresetBundle.hpp" +#include "PresetBundleCache.hpp" #include "PrintConfig.hpp" #include "libslic3r.h" #include "I18N.hpp" @@ -520,6 +522,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 +543,12 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, Forward set_calibrate_printer(""); + { + const auto total_ms = std::chrono::duration_cast( + 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 +957,18 @@ 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(); + + // Helper: collect canonical names of presets in a collection that belong to a bundle. + auto collect_bundle_preset_names = [](const PresetCollection& coll, + const std::string& bundle_id, + std::vector& out) { + for (const Preset& p : coll()) { + if (p.bundle_id == bundle_id) + out.push_back(p.name); + } + }; + // Load bundle metadata from _local directory first fs::path local_dir(folder / PRESET_LOCAL_DIR); if (fs::exists(local_dir)) { @@ -965,16 +987,34 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For metadata.filament_presets.clear(); metadata.printer_presets.clear(); - // Add the profiles + const PresetOrigin origin(PresetOrigin::Kind::LocalBundle, metadata.id); + + // Pre-load from per-file caches; load_presets will skip these. + PresetBundleCache::preload_file_caches(prints, (fs::path(bundle_dir) / PRESET_PRINT_NAME).string(), origin); + PresetBundleCache::preload_file_caches(filaments, (fs::path(bundle_dir) / PRESET_FILAMENT_NAME).string(), origin); + PresetBundleCache::preload_file_caches(printers, (fs::path(bundle_dir) / PRESET_PRINTER_NAME).string(), origin); + + // Record names of preloaded presets in metadata. + collect_bundle_preset_names(prints, metadata.id, metadata.print_presets); + collect_bundle_preset_names(filaments, metadata.id, metadata.filament_presets); + collect_bundle_preset_names(printers, metadata.id, metadata.printer_presets); + + // Load any presets not covered by the cache (callback adds to metadata). 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)); + }, origin); this->filaments.load_presets(bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, [&](Preset& preset) { metadata.filament_presets.push_back(preset.name); - }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); + }, origin); this->printers.load_presets(bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, [&](Preset& preset) { metadata.printer_presets.push_back(preset.name); - }, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id)); + }, origin); + + // Persist any newly-loaded presets back to per-file caches. + PresetBundleCache::update_file_caches(prints, (fs::path(bundle_dir) / PRESET_PRINT_NAME).string()); + PresetBundleCache::update_file_caches(filaments, (fs::path(bundle_dir) / PRESET_FILAMENT_NAME).string()); + PresetBundleCache::update_file_caches(printers, (fs::path(bundle_dir) / PRESET_PRINTER_NAME).string()); + metadata.bundle_type = BundleType::Local; metadata.path = metadata_file.string(); @@ -1002,16 +1042,33 @@ 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__) + const PresetOrigin origin(PresetOrigin::Kind::SubscribedBundle, metadata.id); + + // Pre-load from per-file caches; load_presets will skip these. + PresetBundleCache::preload_file_caches(prints, (fs::path(bundle_dir) / PRESET_PRINT_NAME).string(), origin); + PresetBundleCache::preload_file_caches(filaments, (fs::path(bundle_dir) / PRESET_FILAMENT_NAME).string(), origin); + PresetBundleCache::preload_file_caches(printers, (fs::path(bundle_dir) / PRESET_PRINTER_NAME).string(), origin); + + // Record names of preloaded presets in metadata. + collect_bundle_preset_names(prints, metadata.id, metadata.print_presets); + collect_bundle_preset_names(filaments, metadata.id, metadata.filament_presets); + collect_bundle_preset_names(printers, metadata.id, metadata.printer_presets); + + // Load any presets not covered by the cache (callback adds to metadata). 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)); + }, origin); this->filaments.load_presets(bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, [&](Preset& preset) { metadata.filament_presets.push_back(preset.name); - }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); + }, origin); this->printers.load_presets(bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, [&](Preset& preset) { metadata.printer_presets.push_back(preset.name); - }, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id)); + }, origin); + + // Persist any newly-loaded presets back to per-file caches. + PresetBundleCache::update_file_caches(prints, (fs::path(bundle_dir) / PRESET_PRINT_NAME).string()); + PresetBundleCache::update_file_caches(filaments, (fs::path(bundle_dir) / PRESET_FILAMENT_NAME).string()); + PresetBundleCache::update_file_caches(printers, (fs::path(bundle_dir) / PRESET_PRINTER_NAME).string()); metadata.bundle_type = BundleType::Subscribed; metadata.path = metadata_file.string(); @@ -1023,34 +1080,59 @@ 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 + + // Regular user presets (process/filament/machine) — per-file cache approach. + { + const PresetOrigin user_origin; // Kind::Auto → resolved to User by detect_origin_from_path + + // Pre-load from per-file .cache siblings; load_presets will skip pre-loaded names. + int cached_prints = PresetBundleCache::preload_file_caches(prints, (fs::path(dir_user_presets) / PRESET_PRINT_NAME).string(), user_origin); + int cached_filaments = PresetBundleCache::preload_file_caches(filaments, (fs::path(dir_user_presets) / PRESET_FILAMENT_NAME).string(), user_origin); + int cached_printers = PresetBundleCache::preload_file_caches(printers, (fs::path(dir_user_presets) / PRESET_PRINTER_NAME).string(), user_origin); + BOOST_LOG_TRIVIAL(info) << "PresetBundle: user presets from cache: " + << cached_prints << " process, " + << cached_filaments << " filament, " + << cached_printers << " machine"; + + 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(); + } + 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(); + } + 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); + + // Persist any newly-loaded presets back to per-file caches. + PresetBundleCache::update_file_caches(prints, (fs::path(dir_user_presets) / PRESET_PRINT_NAME).string()); + PresetBundleCache::update_file_caches(filaments, (fs::path(dir_user_presets) / PRESET_FILAMENT_NAME).string()); + PresetBundleCache::update_file_caches(printers, (fs::path(dir_user_presets) / PRESET_PRINTER_NAME).string()); } - 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::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(); @@ -2178,6 +2260,23 @@ std::pair PresetBundle::load_system_pre if (validation_mode) dir = (boost::filesystem::path(data_dir())).make_preferred(); + // Try loading from binary cache first (skips JSON parsing on cache hit). + if (!validation_mode) { + const auto t0 = std::chrono::steady_clock::now(); + PresetBundleCache::SystemPresetsCache cache; + const std::string cache_file = PresetBundleCache::SystemPresetsCache::cache_path(); + if (cache.load(cache_file) && cache.is_valid(dir.string())) { + cache.apply(*this); + update_system_maps(); + const auto ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + BOOST_LOG_TRIVIAL(info) << "PresetBundle: system presets loaded from cache in " << ms << " ms"; + return {PresetsConfigSubstitutions{}, ""}; + } + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " cache miss, falling back to JSON load"; + } + + const auto json_load_t0 = std::chrono::steady_clock::now(); PresetsConfigSubstitutions substitutions; std::string errors_cummulative; bool first = true; @@ -2281,6 +2380,24 @@ std::pair PresetBundle::load_system_pre } this->update_system_maps(); + + { + const auto json_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - json_load_t0).count(); + BOOST_LOG_TRIVIAL(info) << "PresetBundle: system presets loaded from JSON in " << json_ms << " ms"; + } + + // Persist a binary cache so the next startup can skip JSON parsing. + if (!validation_mode && errors_cummulative.empty()) { + const auto save_t0 = std::chrono::steady_clock::now(); + PresetBundleCache::SystemPresetsCache cache; + cache.capture(*this, dir.string()); + cache.save(PresetBundleCache::SystemPresetsCache::cache_path()); + const auto save_ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - save_t0).count(); + BOOST_LOG_TRIVIAL(info) << "PresetBundle: system presets cache 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); diff --git a/src/libslic3r/PresetBundleCache.cpp b/src/libslic3r/PresetBundleCache.cpp new file mode 100644 index 0000000000..eaa729847a --- /dev/null +++ b/src/libslic3r/PresetBundleCache.cpp @@ -0,0 +1,595 @@ +#include "PresetBundleCache.hpp" + +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "Preset.hpp" +#include "PresetBundle.hpp" +#include "PrintConfig.hpp" +#include "Semver.hpp" +#include "Utils.hpp" + +namespace Slic3r { +namespace PresetBundleCache { + +// ------------------------------------------------------------------------- +// Binary cache file format: raw 20-byte header followed by cereal blob. +// ------------------------------------------------------------------------- + +static constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ" +static constexpr uint32_t CACHE_FILE_VERSION = 1; + +#pragma pack(push, 1) +struct CacheFileHeader { + uint32_t magic; + uint32_t file_version; + uint64_t data_size; + uint32_t crc32; +}; +#pragma pack(pop) +static_assert(sizeof(CacheFileHeader) == 20, "CacheFileHeader must be 20 bytes"); + +template +static void save_blob(const std::string& path, const T& obj) +{ + std::ostringstream oss(std::ios::out | std::ios::binary); + { + cereal::BinaryOutputArchive ar(oss); + ar(obj); + } + 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) << "PresetBundleCache: cannot open for writing: " << path; + return; + } + CacheFileHeader hdr; + hdr.magic = CACHE_MAGIC; + hdr.file_version = CACHE_FILE_VERSION; + hdr.data_size = static_cast(blob.size()); + hdr.crc32 = crc.checksum(); + ofs.write(reinterpret_cast(&hdr), sizeof(hdr)); + ofs.write(blob.data(), static_cast(blob.size())); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "PresetBundleCache: write failed (" << path << "): " << e.what(); + } +} + +template +static bool load_blob(const std::string& path, T& obj) +{ + try { + boost::nowide::ifstream ifs(path, std::ios::binary); + if (!ifs.is_open()) + return false; + + CacheFileHeader hdr; + if (!ifs.read(reinterpret_cast(&hdr), sizeof(hdr))) + return false; + if (hdr.magic != CACHE_MAGIC || hdr.file_version != CACHE_FILE_VERSION) + return false; + if (hdr.data_size == 0 || hdr.data_size > 512u * 1024u * 1024u) + return false; + + std::string blob(hdr.data_size, '\0'); + if (!ifs.read(&blob[0], static_cast(hdr.data_size))) + return false; + + boost::crc_32_type crc; + crc.process_bytes(blob.data(), blob.size()); + if (crc.checksum() != hdr.crc32) { + BOOST_LOG_TRIVIAL(warning) << "PresetBundleCache: CRC32 mismatch: " << path; + return false; + } + + std::istringstream iss(blob, std::ios::in | std::ios::binary); + cereal::BinaryInputArchive ar(iss); + ar(obj); + return true; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "PresetBundleCache: load failed (" << path << "): " << e.what(); + return false; + } +} + +// ------------------------------------------------------------------------- +// Helpers +// ------------------------------------------------------------------------- + +static std::string vendor_root_json(const std::string& system_dir, const std::string& vendor_id) +{ + return (boost::filesystem::path(system_dir) / (vendor_id + ".json")).make_preferred().string(); +} + +// ------------------------------------------------------------------------- +// SystemPresetsCache +// ------------------------------------------------------------------------- + +std::string SystemPresetsCache::cache_path() +{ + return (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR / "system_presets_cache.cache") + .make_preferred().string(); +} + +bool SystemPresetsCache::is_valid(const std::string& system_dir) const +{ + if (format_version != FORMAT_VERSION) + return false; + if (config_options_count != print_config_def.options.size()) + return false; + + std::map current; + try { + for (const auto& entry : boost::filesystem::directory_iterator(system_dir)) { + const std::string path = entry.path().string(); + if (!Slic3r::is_json_file(path)) + continue; + const std::string vendor_name = entry.path().stem().string(); + Semver ver = get_version_from_json(path); + if (ver.valid()) + current[vendor_name] = ver.to_string(); + } + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "PresetBundleCache: directory scan failed: " << e.what(); + return false; + } + + if (current.size() != vendor_versions.size()) + return false; + for (const auto& [name, ver] : current) { + auto it = vendor_versions.find(name); + if (it == vendor_versions.end() || it->second != ver) + return false; + } + return true; +} + +void SystemPresetsCache::capture(const PresetBundle& bundle, const std::string& system_dir) +{ + format_version = FORMAT_VERSION; + config_options_count = print_config_def.options.size(); + vendor_versions.clear(); + vendor_profiles.clear(); + print_presets.clear(); + filament_presets.clear(); + printer_presets.clear(); + sla_print_presets.clear(); + sla_material_presets.clear(); + config_maps = bundle.m_config_maps; + filament_id_maps = bundle.m_filament_id_maps; + + for (const auto& [id, vp] : bundle.vendors) { + CachedVendorProfile cvp; + cvp.id = vp.id; + cvp.name = vp.name; + cvp.config_version = vp.config_version.valid() ? vp.config_version.to_string() : ""; + cvp.config_update_url = vp.config_update_url; + cvp.changelog_url = vp.changelog_url; + + for (const auto& model : vp.models) { + CachedPrinterModel cm; + cm.id = model.id; + cm.name = model.name; + cm.model_id = model.model_id; + cm.family = model.family; + cm.technology = static_cast(model.technology); + for (const auto& v : model.variants) + cm.variants.push_back({v.name}); + cm.default_materials = model.default_materials; + cm.not_support_bed_types = model.not_support_bed_types; + cm.bed_model = model.bed_model; + cm.bed_texture = model.bed_texture; + cm.image_bed_type = model.image_bed_type; + cm.bottom_texture_end_name = model.bottom_texture_end_name; + cm.use_double_extruder_default_texture = model.use_double_extruder_default_texture; + cm.bottom_texture_rect = model.bottom_texture_rect; + cm.middle_texture_rect = model.middle_texture_rect; + cm.hotend_model = model.hotend_model; + cvp.models.push_back(std::move(cm)); + } + + for (const auto& f : vp.default_filaments) + cvp.default_filaments.push_back(f); + for (const auto& m : vp.default_sla_materials) + cvp.default_sla_materials.push_back(m); + + vendor_profiles.push_back(std::move(cvp)); + + Semver ver = get_version_from_json(vendor_root_json(system_dir, id)); + vendor_versions[id] = ver.valid() ? ver.to_string() : ""; + } + + auto capture_col = [](const PresetCollection& coll, std::vector& out) { + for (const Preset& p : coll()) { + if (!p.is_system) + continue; + CachedPreset cp; + cp.type = static_cast(p.type); + cp.name = p.name; + cp.alias = p.alias; + cp.file = p.file; + cp.version = p.version.valid() ? p.version.to_string() : ""; + cp.vendor_id = (p.vendor != nullptr) ? p.vendor->id : ""; + cp.filament_id = p.filament_id; + cp.setting_id = p.setting_id; + cp.description = p.description; + cp.renamed_from = p.renamed_from; + cp.is_system = p.is_system; + cp.is_visible = p.is_visible; + cp.m_from_orca_filament_lib = p.m_from_orca_filament_lib; + cp.config = p.config; + out.push_back(std::move(cp)); + } + }; + + capture_col(bundle.prints, print_presets); + capture_col(bundle.filaments, filament_presets); + capture_col(bundle.printers, printer_presets); + capture_col(bundle.sla_prints, sla_print_presets); + capture_col(bundle.sla_materials, sla_material_presets); +} + +void SystemPresetsCache::apply(PresetBundle& bundle) const +{ + bundle.reset(false); + + for (const auto& cvp : vendor_profiles) { + VendorProfile vp(cvp.id); + vp.name = cvp.name; + vp.config_update_url = cvp.config_update_url; + vp.changelog_url = cvp.changelog_url; + if (!cvp.config_version.empty()) { + auto v = Semver::parse(cvp.config_version); + if (v) vp.config_version = *v; + } + + for (const auto& cm : cvp.models) { + VendorProfile::PrinterModel model; + model.id = cm.id; + model.name = cm.name; + model.model_id = cm.model_id; + model.family = cm.family; + model.technology = static_cast(cm.technology); + for (const auto& v : cm.variants) + model.variants.emplace_back(v.name); + model.default_materials = cm.default_materials; + model.not_support_bed_types = cm.not_support_bed_types; + model.bed_model = cm.bed_model; + model.bed_texture = cm.bed_texture; + model.image_bed_type = cm.image_bed_type; + model.bottom_texture_end_name = cm.bottom_texture_end_name; + model.use_double_extruder_default_texture = cm.use_double_extruder_default_texture; + model.bottom_texture_rect = cm.bottom_texture_rect; + model.middle_texture_rect = cm.middle_texture_rect; + model.hotend_model = cm.hotend_model; + vp.models.push_back(std::move(model)); + } + + for (const auto& f : cvp.default_filaments) + vp.default_filaments.insert(f); + for (const auto& m : cvp.default_sla_materials) + vp.default_sla_materials.insert(m); + + bundle.vendors.emplace(cvp.id, std::move(vp)); + } + + auto apply_col = [&bundle](const std::vector& cached, + PresetCollection& coll, + bool is_filaments) { + for (const auto& cp : cached) { + Semver version; + if (!cp.version.empty()) { + auto v = Semver::parse(cp.version); + if (v) version = *v; + } + DynamicPrintConfig config = cp.config; + Preset& p = coll.load_preset(cp.file, cp.name, std::move(config), /*select=*/false, 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; + + if (!cp.vendor_id.empty()) { + auto it = bundle.vendors.find(cp.vendor_id); + if (it != bundle.vendors.end()) + p.vendor = &it->second; + } + + if (is_filaments) + coll.set_printer_hold_alias(p.alias, p); + } + }; + + apply_col(print_presets, bundle.prints, false); + apply_col(filament_presets, bundle.filaments, true); + apply_col(printer_presets, bundle.printers, false); + apply_col(sla_print_presets, bundle.sla_prints, false); + apply_col(sla_material_presets, bundle.sla_materials, false); + + bundle.m_config_maps = config_maps; + bundle.m_filament_id_maps = filament_id_maps; + // Caller must invoke bundle.update_system_maps() after this (it is private to PresetBundle). +} + +bool SystemPresetsCache::load(const std::string& path) +{ + return load_blob(path, *this); +} + +void SystemPresetsCache::save(const std::string& path) const +{ + save_blob(path, *this); +} + +// ------------------------------------------------------------------------- +// PresetFileCache +// ------------------------------------------------------------------------- + +std::string PresetFileCache::cache_path_for(const std::string& json_path) +{ + boost::filesystem::path p(json_path); + p.replace_extension(".cache"); + return p.string(); +} + +bool PresetFileCache::is_valid(const std::string& json_path) const +{ + if (format_version != FORMAT_VERSION) + return false; + if (config_options_count != print_config_def.options.size()) + return false; + + namespace fs = boost::filesystem; + const fs::path json(json_path); + if (!fs::exists(json)) + return false; + + int64_t cur_json_mtime = 0; + try { + cur_json_mtime = static_cast(fs::last_write_time(json)); + } catch (...) { return false; } + if (json_mtime != cur_json_mtime) + return false; + + fs::path info = json; + info.replace_extension(".info"); + int64_t cur_info_mtime = 0; + if (fs::exists(info)) { + try { + cur_info_mtime = static_cast(fs::last_write_time(info)); + } catch (...) { return false; } + } + return info_mtime == cur_info_mtime; +} + +// ------------------------------------------------------------------------- +// preload_file_caches / update_file_caches +// ------------------------------------------------------------------------- + +int preload_file_caches(PresetCollection& coll, const std::string& dir_path, + const PresetOrigin& origin) +{ + namespace fs = boost::filesystem; + const fs::path dir(dir_path); + if (!fs::exists(dir)) + return 0; + + // Step 1: collect .json paths (fast, sequential). + std::vector json_paths; + try { + for (const auto& entry : fs::directory_iterator(dir)) { + if (fs::is_regular_file(entry.path()) && + entry.path().extension().string() == ".json") + json_paths.push_back(entry.path().string()); + } + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "PresetBundleCache: dir scan failed (" + << dir_path << "): " << e.what(); + return 0; + } + + // Step 2: load + deserialize each .cache file in parallel. + // Each slot is independent - no shared state touched here. + struct LoadResult { + std::string json_path; + std::string canonical_name; + PresetFileCache pfc; + bool valid = false; + }; + std::vector results(json_paths.size()); + + tbb::parallel_for(tbb::blocked_range(0, json_paths.size()), + [&](const tbb::blocked_range& range) { + for (size_t i = range.begin(); i < range.end(); ++i) { + const std::string& json_path = json_paths[i]; + const std::string cache_path = PresetFileCache::cache_path_for(json_path); + + PresetFileCache pfc; + if (!load_blob(cache_path, pfc) || !pfc.is_valid(json_path)) + continue; + + const std::string stem = fs::path(json_path).stem().string(); + results[i].json_path = json_path; + results[i].canonical_name = get_preset_canonical_name(stem, origin); + results[i].pfc = std::move(pfc); + results[i].valid = true; + } + }); + + // Step 3: sequential merge into the collection (load_preset uses a lock). + const bool is_filaments = (coll.type() == Preset::TYPE_FILAMENT); + int loaded = 0; + for (auto& r : results) { + if (!r.valid) + continue; + + Semver version; + if (!r.pfc.preset.version.empty()) { + auto v = Semver::parse(r.pfc.preset.version); + if (v) version = *v; + } + + DynamicPrintConfig config = r.pfc.preset.config; + Preset& p = coll.load_preset(r.json_path, r.canonical_name, + std::move(config), /*select=*/false, version); + p.is_system = false; + p.is_visible = r.pfc.preset.is_visible; + p.alias = r.pfc.preset.alias; + p.filament_id = r.pfc.preset.filament_id; + p.setting_id = r.pfc.preset.setting_id; + p.description = r.pfc.preset.description; + p.base_id = r.pfc.preset.base_id; + p.user_id = r.pfc.preset.user_id; + p.sync_info = r.pfc.preset.sync_info; + p.updated_time = r.pfc.preset.updated_time; + p.renamed_from = r.pfc.preset.renamed_from; + p.bundle_id = origin.bundle_id; + + if (is_filaments) + coll.set_printer_hold_alias(p.alias, p); + + ++loaded; + } + return loaded; +} + +void update_file_caches(const PresetCollection& coll, const std::string& dir_path) +{ + namespace fs = boost::filesystem; + const std::string norm_dir = fs::path(dir_path).make_preferred().string(); + + // Step 1: collect candidate presets (sequential, fast). + std::vector candidates; + for (const Preset& p : coll()) { + if (p.is_system || p.is_default || p.file.empty()) + continue; + const std::string norm_file = fs::path(p.file).make_preferred().string(); + if (norm_file.rfind(norm_dir, 0) != 0) + continue; + candidates.push_back(&p); + } + + // Step 2: check + write each .cache file in parallel. + // Each write targets an independent file path - no shared state. + tbb::parallel_for(tbb::blocked_range(0, candidates.size()), + [&](const tbb::blocked_range& range) { + for (size_t i = range.begin(); i < range.end(); ++i) { + const Preset& p = *candidates[i]; + const std::string cache_path = PresetFileCache::cache_path_for(p.file); + + PresetFileCache existing; + if (load_blob(cache_path, existing) && existing.is_valid(p.file)) + continue; + + PresetFileCache pfc; + pfc.format_version = PresetFileCache::FORMAT_VERSION; + pfc.config_options_count = print_config_def.options.size(); + + try { + pfc.json_mtime = static_cast( + fs::last_write_time(fs::path(p.file))); + } catch (...) {} + + fs::path info_path = fs::path(p.file); + info_path.replace_extension(".info"); + if (fs::exists(info_path)) { + try { + pfc.info_mtime = static_cast( + fs::last_write_time(info_path)); + } catch (...) {} + } + + CachedPreset& cp = pfc.preset; + cp.type = static_cast(p.type); + cp.name = p.name; + cp.alias = p.alias; + cp.file = p.file; + cp.version = p.version.valid() ? p.version.to_string() : ""; + cp.filament_id = p.filament_id; + cp.setting_id = p.setting_id; + cp.description = p.description; + cp.base_id = p.base_id; + cp.user_id = p.user_id; + cp.sync_info = p.sync_info; + cp.updated_time = p.updated_time; + cp.renamed_from = p.renamed_from; + cp.is_system = false; + cp.is_visible = p.is_visible; + cp.config = p.config; + + save_blob(cache_path, pfc); + } + }); +} + +void write_file_cache(const Preset& preset) +{ + if (preset.is_system || preset.is_default || preset.is_project_embedded || preset.file.empty()) + return; + + namespace fs = boost::filesystem; + const std::string cache_path = PresetFileCache::cache_path_for(preset.file); + + PresetFileCache pfc; + pfc.format_version = PresetFileCache::FORMAT_VERSION; + pfc.config_options_count = print_config_def.options.size(); + + try { + pfc.json_mtime = static_cast(fs::last_write_time(fs::path(preset.file))); + } catch (...) {} + + fs::path info_path = fs::path(preset.file); + info_path.replace_extension(".info"); + if (fs::exists(info_path)) { + try { + pfc.info_mtime = static_cast(fs::last_write_time(info_path)); + } catch (...) {} + } + + CachedPreset& cp = pfc.preset; + cp.type = static_cast(preset.type); + cp.name = preset.name; + cp.alias = preset.alias; + cp.file = preset.file; + cp.version = preset.version.valid() ? preset.version.to_string() : ""; + cp.filament_id = preset.filament_id; + cp.setting_id = preset.setting_id; + cp.description = preset.description; + cp.base_id = preset.base_id; + cp.user_id = preset.user_id; + cp.sync_info = preset.sync_info; + cp.updated_time = preset.updated_time; + cp.renamed_from = preset.renamed_from; + cp.is_system = false; + cp.is_visible = preset.is_visible; + cp.config = preset.config; + + save_blob(cache_path, pfc); +} + +} // namespace PresetBundleCache +} // namespace Slic3r diff --git a/src/libslic3r/PresetBundleCache.hpp b/src/libslic3r/PresetBundleCache.hpp new file mode 100644 index 0000000000..a292072a4a --- /dev/null +++ b/src/libslic3r/PresetBundleCache.hpp @@ -0,0 +1,170 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "Preset.hpp" +#include "PrintConfig.hpp" + +namespace Slic3r { + +class PresetBundle; + +namespace PresetBundleCache { + +// ---- Vendor profile structures ---- + +struct CachedPrinterVariant { + std::string name; + template void serialize(Archive& ar) { ar(name); } +}; + +struct CachedPrinterModel { + std::string id, name, model_id, family; + int technology = 0; // PrinterTechnology enum + std::vector variants; + std::vector default_materials; + std::vector not_support_bed_types; + std::string bed_model, bed_texture, image_bed_type; + std::string bottom_texture_end_name, use_double_extruder_default_texture; + std::string bottom_texture_rect, middle_texture_rect, hotend_model; + + template + 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); + } +}; + +struct CachedVendorProfile { + std::string id, name, config_version, config_update_url, changelog_url; + std::vector models; + std::vector default_filaments; + std::vector default_sla_materials; + + template + void serialize(Archive& ar) + { + ar(id, name, config_version, config_update_url, changelog_url, + models, default_filaments, default_sla_materials); + } +}; + +// ---- Per-preset cache entry (shared by system and per-file caches) ---- + +struct CachedPreset { + int type = 0; // Preset::Type + std::string name, alias, file, version; + std::string vendor_id; // reconstruct Preset::vendor pointer (system only) + std::string filament_id, setting_id, description; + std::string base_id, user_id, sync_info; + long long updated_time = 0; + std::vector renamed_from; + bool is_system = true; + bool is_visible = true; + bool m_from_orca_filament_lib = false; + DynamicPrintConfig config; + + template + void serialize(Archive& ar) + { + ar(type, name, alias, file, version, vendor_id, filament_id, setting_id, + description, base_id, user_id, sync_info, updated_time, renamed_from, + is_system, is_visible, m_from_orca_filament_lib, config); + } +}; + +// ---- System preset cache ---- +// Stored next to vendor JSONs: /system/system_presets_cache.bin + +struct SystemPresetsCache { + static constexpr uint32_t FORMAT_VERSION = 2; + + uint32_t format_version = FORMAT_VERSION; + size_t config_options_count = 0; + + std::map vendor_versions; + std::vector vendor_profiles; + + std::vector print_presets; + std::vector filament_presets; + std::vector printer_presets; + std::vector sla_print_presets; + std::vector sla_material_presets; + + std::map config_maps; + std::map filament_id_maps; + + template + void serialize(Archive& ar) + { + ar(format_version, config_options_count, vendor_versions, + vendor_profiles, + print_presets, filament_presets, printer_presets, + sla_print_presets, sla_material_presets, + config_maps, filament_id_maps); + } + + static std::string cache_path(); + bool is_valid(const std::string& system_dir) const; + void capture(const PresetBundle& bundle, const std::string& system_dir); + void apply(PresetBundle& bundle) const; + bool load(const std::string& path); + void save(const std::string& path) const; +}; + +// ---- Per-file preset cache ---- +// Each .json gets a sibling .cache file. +// Invalidated when json or info mtime changes, or when the app binary changes. + +struct PresetFileCache { + static constexpr uint32_t FORMAT_VERSION = 1; + + uint32_t format_version = FORMAT_VERSION; + size_t config_options_count = 0; + int64_t json_mtime = 0; // last_write_time of .json file + int64_t info_mtime = 0; // last_write_time of .info file (0 = doesn't exist) + CachedPreset preset; + + template + void serialize(Archive& ar) + { + ar(format_version, config_options_count, json_mtime, info_mtime, preset); + } + + // Returns the path of the .cache file sibling to json_path. + static std::string cache_path_for(const std::string& json_path); + + // Returns true when the stored mtimes still match the current filesystem state. + bool is_valid(const std::string& json_path) const; +}; + +// Pre-load cached presets from all valid .cache files found in dir_path. +// Presets are inserted into coll via load_preset() so that a subsequent +// load_presets() call will skip them. Returns the count of presets loaded. +int preload_file_caches(PresetCollection& coll, const std::string& dir_path, + const PresetOrigin& origin); + +// Write (or overwrite) .cache files for presets in coll whose json .file is +// inside dir_path and whose existing .cache (if any) is stale or missing. +void update_file_caches(const PresetCollection& coll, const std::string& dir_path); + +// Write (or overwrite) the .cache file for a single preset immediately after +// its .json has been written to disk. Safe to call from Preset::save(). +// Does nothing for system, default, or project-embedded presets. +void write_file_cache(const Preset& preset); + +} // namespace PresetBundleCache +} // namespace Slic3r