diff --git a/src/ifcviewer/SceneLoader.cpp b/src/ifcviewer/SceneLoader.cpp index a78fee1adb..45c2277e10 100644 --- a/src/ifcviewer/SceneLoader.cpp +++ b/src/ifcviewer/SceneLoader.cpp @@ -49,6 +49,8 @@ SceneLoader::SceneLoader(ViewportWindow* viewport, QObject* parent) SceneLoader::~SceneLoader() { joinSidecarThread(); joinDataSourceThreads(); + if (sidecar_write_thread_.joinable()) + sidecar_write_thread_.join(); } void SceneLoader::joinSidecarThread() { @@ -389,15 +391,20 @@ void SceneLoader::onStreamerFinished() { if (auto* file = model.streamer->ifcFile()) { georef = computeModelGeoref(file); } - QElapsedTimer write_timer; write_timer.start(); SidecarData data = model.sidecar_builder->finalize(georef, model.streamed_elements); // Lay geometry out in streaming-chunk order + bake the chunk TOC // (v14) so it streams as one contiguous range per chunk. reorderSidecarByMorton(data); - const bool ok = writeSidecar(model.file_path.toStdString(), data); - std::fprintf(stderr, - "[info] Sidecar finalize + write: %lld ms (%s)\n", - (long long)write_timer.elapsed(), ok ? "ok" : "FAILED"); + // Compress + write the .ifcview on a background thread so the + // seconds of zstd on a large model don't freeze the UI right at + // 100%. The geometry is already on the GPU and the sidecar is + // only a cache for the next open, so it finishes asynchronously + // (joined before the next write / in the destructor). + if (sidecar_write_thread_.joinable()) sidecar_write_thread_.join(); + sidecar_write_thread_ = std::thread( + [ifc_path = model.file_path.toStdString(), sd = std::move(data)]() { + writeSidecar(ifc_path, sd); + }); model.sidecar_builder.reset(); } diff --git a/src/ifcviewer/SceneLoader.h b/src/ifcviewer/SceneLoader.h index 4a03247de4..bcdcbf0f91 100644 --- a/src/ifcviewer/SceneLoader.h +++ b/src/ifcviewer/SceneLoader.h @@ -173,6 +173,10 @@ private: uint32_t next_session_model_id_ = 1; uint32_t loading_session_model_id_ = 0; std::thread sidecar_read_thread_; + // Background .ifcview compress + write, so the seconds of zstd on a big + // model don't freeze the UI at 100%. Joined before the next write and in + // the destructor so a pending write always completes. + std::thread sidecar_write_thread_; // One thread per sidecar-hit model while its .rdb/.ifc opens in the // background. Joined only at destruction so a slow SPF parse on model // A never blocks the sidecar-hit path of model B. diff --git a/src/ifcviewer/SidecarCache.cpp b/src/ifcviewer/SidecarCache.cpp index 4c894f44e0..73ca7c7ab9 100644 --- a/src/ifcviewer/SidecarCache.cpp +++ b/src/ifcviewer/SidecarCache.cpp @@ -45,8 +45,11 @@ #include "SidecarCache.h" #include "SidecarCompress.h" +#include +#include #include #include +#include // The baker (writeSidecar) compresses — desktop only; the web build never bakes // and links a decompress-only zstd. Everything from here to writeSidecar's end @@ -184,20 +187,54 @@ bool writeSidecar(const std::string& ifc_path, const SidecarData& data) { const long geom_start = ftell(f); std::vector chunks = data.chunks; // fill blob offsets below - std::vector vraw, iraw; - for (auto& sidecar_chunk : chunks) { - extractChunkGeometry(data, sidecar_chunk, vraw, iraw); - auto vz = SidecarCompress::compress(vraw.data(), vraw.size(), kSidecarZstdLevel); - auto iz = SidecarCompress::compress(iraw.data(), iraw.size(), kSidecarZstdLevel); - if ((vraw.size() && vz.empty()) || (iraw.size() && iz.empty())) { fclose(f); return false; } + + // Compress every chunk's geometry in parallel — zstd is the bulk of the bake + // cost — then write the frames serially so their offsets stay contiguous. + struct ChunkBlob { + std::vector vz, iz; + std::size_t v_raw = 0, i_raw = 0; + }; + std::vector blobs(chunks.size()); + std::atomic compress_ok{true}; + { + const unsigned hw = std::max(1u, std::thread::hardware_concurrency()); + const std::size_t worker_count = + std::min(hw, std::max(std::size_t(1), chunks.size())); + std::atomic next{0}; + auto worker = [&]() { + std::vector vraw, iraw; + for (std::size_t idx = next.fetch_add(1); idx < chunks.size(); + idx = next.fetch_add(1)) { + extractChunkGeometry(data, chunks[idx], vraw, iraw); + blobs[idx].v_raw = vraw.size(); + blobs[idx].i_raw = iraw.size(); + blobs[idx].vz = SidecarCompress::compress(vraw.data(), vraw.size(), kSidecarZstdLevel); + blobs[idx].iz = SidecarCompress::compress(iraw.data(), iraw.size(), kSidecarZstdLevel); + if ((vraw.size() && blobs[idx].vz.empty()) || + (iraw.size() && blobs[idx].iz.empty())) { + compress_ok.store(false, std::memory_order_relaxed); + } + } + }; + std::vector pool; + pool.reserve(worker_count > 0 ? worker_count - 1 : 0); + for (std::size_t i = 1; i < worker_count; ++i) pool.emplace_back(worker); + worker(); // the calling thread participates too + for (auto& th : pool) th.join(); + } + if (!compress_ok.load()) { fclose(f); return false; } + + for (std::size_t idx = 0; idx < chunks.size(); ++idx) { + auto& sidecar_chunk = chunks[idx]; + const ChunkBlob& blob = blobs[idx]; sidecar_chunk.v_comp_off = std::uint64_t(ftell(f) - geom_start); - sidecar_chunk.v_comp_size = vz.size(); - sidecar_chunk.v_raw_size = vraw.size(); - if (!vz.empty() && !write_bytes(vz.data(), vz.size())) { fclose(f); return false; } + sidecar_chunk.v_comp_size = blob.vz.size(); + sidecar_chunk.v_raw_size = blob.v_raw; + if (!blob.vz.empty() && !write_bytes(blob.vz.data(), blob.vz.size())) { fclose(f); return false; } sidecar_chunk.i_comp_off = std::uint64_t(ftell(f) - geom_start); - sidecar_chunk.i_comp_size = iz.size(); - sidecar_chunk.i_raw_size = iraw.size(); - if (!iz.empty() && !write_bytes(iz.data(), iz.size())) { fclose(f); return false; } + sidecar_chunk.i_comp_size = blob.iz.size(); + sidecar_chunk.i_raw_size = blob.i_raw; + if (!blob.iz.empty() && !write_bytes(blob.iz.data(), blob.iz.size())) { fclose(f); return false; } } const long geom_end = ftell(f); if (geom_start < 0 || geom_end < 0) { fclose(f); return false; }