mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-19 06:39:13 +00:00
ifcviewer: don't block the UI while baking the .ifcview at 100%
Opening a fresh .ifc streams geometry to the GPU, then bakes the .ifcview cache. That bake — reorder + per-chunk zstd (level 19) — ran synchronously in SceneLoader::onStreamerFinished, which is a QueuedConnection slot on the main thread, so it froze the UI right as the progress bar hit 100% (≈15s of zstd for a 130 MB-geometry model). - Move the compress + writeSidecar onto a background thread. The geometry is already resident and the sidecar is only a cache for the next open, so the viewport is interactive the instant streaming finishes; the write is joined before the next write and in the destructor. - Parallelise the per-chunk zstd across hardware_concurrency threads (compress all chunks, then write serially to keep contiguous offsets) so the background write also finishes quickly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,8 @@ SceneLoader::SceneLoader(ViewportWindow* viewport, QObject* parent)
|
|||||||
SceneLoader::~SceneLoader() {
|
SceneLoader::~SceneLoader() {
|
||||||
joinSidecarThread();
|
joinSidecarThread();
|
||||||
joinDataSourceThreads();
|
joinDataSourceThreads();
|
||||||
|
if (sidecar_write_thread_.joinable())
|
||||||
|
sidecar_write_thread_.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SceneLoader::joinSidecarThread() {
|
void SceneLoader::joinSidecarThread() {
|
||||||
@@ -389,15 +391,20 @@ void SceneLoader::onStreamerFinished() {
|
|||||||
if (auto* file = model.streamer->ifcFile()) {
|
if (auto* file = model.streamer->ifcFile()) {
|
||||||
georef = computeModelGeoref(file);
|
georef = computeModelGeoref(file);
|
||||||
}
|
}
|
||||||
QElapsedTimer write_timer; write_timer.start();
|
|
||||||
SidecarData data = model.sidecar_builder->finalize(georef, model.streamed_elements);
|
SidecarData data = model.sidecar_builder->finalize(georef, model.streamed_elements);
|
||||||
// Lay geometry out in streaming-chunk order + bake the chunk TOC
|
// Lay geometry out in streaming-chunk order + bake the chunk TOC
|
||||||
// (v14) so it streams as one contiguous range per chunk.
|
// (v14) so it streams as one contiguous range per chunk.
|
||||||
reorderSidecarByMorton(data);
|
reorderSidecarByMorton(data);
|
||||||
const bool ok = writeSidecar(model.file_path.toStdString(), data);
|
// Compress + write the .ifcview on a background thread so the
|
||||||
std::fprintf(stderr,
|
// seconds of zstd on a large model don't freeze the UI right at
|
||||||
"[info] Sidecar finalize + write: %lld ms (%s)\n",
|
// 100%. The geometry is already on the GPU and the sidecar is
|
||||||
(long long)write_timer.elapsed(), ok ? "ok" : "FAILED");
|
// 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();
|
model.sidecar_builder.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,10 @@ private:
|
|||||||
uint32_t next_session_model_id_ = 1;
|
uint32_t next_session_model_id_ = 1;
|
||||||
uint32_t loading_session_model_id_ = 0;
|
uint32_t loading_session_model_id_ = 0;
|
||||||
std::thread sidecar_read_thread_;
|
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
|
// 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
|
// background. Joined only at destruction so a slow SPF parse on model
|
||||||
// A never blocks the sidecar-hit path of model B.
|
// A never blocks the sidecar-hit path of model B.
|
||||||
|
|||||||
@@ -45,8 +45,11 @@
|
|||||||
#include "SidecarCache.h"
|
#include "SidecarCache.h"
|
||||||
#include "SidecarCompress.h"
|
#include "SidecarCompress.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
// The baker (writeSidecar) compresses — desktop only; the web build never bakes
|
// The baker (writeSidecar) compresses — desktop only; the web build never bakes
|
||||||
// and links a decompress-only zstd. Everything from here to writeSidecar's end
|
// 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);
|
const long geom_start = ftell(f);
|
||||||
|
|
||||||
std::vector<SidecarChunk> chunks = data.chunks; // fill blob offsets below
|
std::vector<SidecarChunk> chunks = data.chunks; // fill blob offsets below
|
||||||
std::vector<std::uint8_t> vraw, iraw;
|
|
||||||
for (auto& sidecar_chunk : chunks) {
|
// Compress every chunk's geometry in parallel — zstd is the bulk of the bake
|
||||||
extractChunkGeometry(data, sidecar_chunk, vraw, iraw);
|
// cost — then write the frames serially so their offsets stay contiguous.
|
||||||
auto vz = SidecarCompress::compress(vraw.data(), vraw.size(), kSidecarZstdLevel);
|
struct ChunkBlob {
|
||||||
auto iz = SidecarCompress::compress(iraw.data(), iraw.size(), kSidecarZstdLevel);
|
std::vector<std::uint8_t> vz, iz;
|
||||||
if ((vraw.size() && vz.empty()) || (iraw.size() && iz.empty())) { fclose(f); return false; }
|
std::size_t v_raw = 0, i_raw = 0;
|
||||||
|
};
|
||||||
|
std::vector<ChunkBlob> blobs(chunks.size());
|
||||||
|
std::atomic<bool> compress_ok{true};
|
||||||
|
{
|
||||||
|
const unsigned hw = std::max(1u, std::thread::hardware_concurrency());
|
||||||
|
const std::size_t worker_count =
|
||||||
|
std::min<std::size_t>(hw, std::max<std::size_t>(std::size_t(1), chunks.size()));
|
||||||
|
std::atomic<std::size_t> next{0};
|
||||||
|
auto worker = [&]() {
|
||||||
|
std::vector<std::uint8_t> 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<std::thread> 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_off = std::uint64_t(ftell(f) - geom_start);
|
||||||
sidecar_chunk.v_comp_size = vz.size();
|
sidecar_chunk.v_comp_size = blob.vz.size();
|
||||||
sidecar_chunk.v_raw_size = vraw.size();
|
sidecar_chunk.v_raw_size = blob.v_raw;
|
||||||
if (!vz.empty() && !write_bytes(vz.data(), vz.size())) { fclose(f); return false; }
|
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_off = std::uint64_t(ftell(f) - geom_start);
|
||||||
sidecar_chunk.i_comp_size = iz.size();
|
sidecar_chunk.i_comp_size = blob.iz.size();
|
||||||
sidecar_chunk.i_raw_size = iraw.size();
|
sidecar_chunk.i_raw_size = blob.i_raw;
|
||||||
if (!iz.empty() && !write_bytes(iz.data(), iz.size())) { fclose(f); return false; }
|
if (!blob.iz.empty() && !write_bytes(blob.iz.data(), blob.iz.size())) { fclose(f); return false; }
|
||||||
}
|
}
|
||||||
const long geom_end = ftell(f);
|
const long geom_end = ftell(f);
|
||||||
if (geom_start < 0 || geom_end < 0) { fclose(f); return false; }
|
if (geom_start < 0 || geom_end < 0) { fclose(f); return false; }
|
||||||
|
|||||||
Reference in New Issue
Block a user