diff --git a/src/ifcviewer-full/MainWindow.cpp b/src/ifcviewer-full/MainWindow.cpp index fabd008daf..8a818f948e 100644 --- a/src/ifcviewer-full/MainWindow.cpp +++ b/src/ifcviewer-full/MainWindow.cpp @@ -295,7 +295,7 @@ void MainWindow::writeSidecarForModel(uint32_t mid) { viewport_->applyLodExtension(mid, sd); QElapsedTimer t; t.start(); - bool ok = writeSidecar(loader_->filePath(mid).toStdString(), sd, loader_->fileSize(mid)); + bool ok = writeSidecar(loader_->filePath(mid).toStdString(), sd); qDebug(" Sidecar write: %lld ms (%s)", t.elapsed(), ok ? "ok" : "FAILED"); } diff --git a/src/ifcviewer/README.md b/src/ifcviewer/README.md index eb1e0d88df..301322851c 100644 --- a/src/ifcviewer/README.md +++ b/src/ifcviewer/README.md @@ -116,7 +116,7 @@ engine with a Qt6 interface and OpenGL 4.5 rendering. | `InstancedGeometry.h` | Shared structs: `MeshInfo`, `InstanceCpu`, `InstanceGpu`, chunk records | | `BvhAccel.h/cpp` | Median-split BVH builder; operates on instance world-AABBs | | `LodBuilder.h/cpp` | Post-stream decimation of unique meshes via meshoptimizer (`simplifySloppy`) | -| `SidecarCache.h/cpp` | Raw binary `.ifcview` (v7) sidecar read/write | +| `SidecarCache.h/cpp` | Raw binary `.ifcview` (v8) sidecar read/write | | `AppSettings.h/cpp` | Persisted preferences (geometry library, stats overlay, backface culling) | | `SettingsWindow.h/cpp` | Settings dialog | | `CMakeLists.txt` | Build configuration | @@ -293,14 +293,13 @@ while stack not empty: Depth 64 is enough for billions of items on any balanced tree. The stack is on the C++ stack, zero per-frame allocation. -#### Sidecar format (`.ifcview`, v7) +#### Sidecar format (`.ifcview`, v8) Raw memory dump, Blender-`.blend`-style — no serialisation, no parsing. Stores everything needed to skip the `IfcGeom::Iterator` pass: ``` SidecarHeader (magic "IFVW", version, endian, ...) -uint64_t source_file_size uint32_t + uint8_t[] vertex data (12 B/vert quantized; per-mesh basis in MeshInfo) uint32_t + uint32_t[] index data (mesh-local) uint32_t + MeshInfo[] per-unique-mesh metadata (56 B each, incl. LOD1 slice) @@ -309,8 +308,10 @@ uint32_t + PackedElementInfo[] element tree records uint32_t + char[] string table ``` -Staleness check: `source_file_size` vs actual file size. Mismatched → -reject and rebuild. Endianness marker rejects cross-arch caches. +Sidecar path is the source stem + `.ifcview` — `foo.ifc` and `foo.ifcdb/` +both map to `foo.ifcview`, so the same cache serves either source format. +Staleness is user-managed: delete the sidecar to force a rebuild. +Endianness marker rejects cross-arch caches. Sidecars store the raw `object_id` / `model_id` values from the session that wrote them. On load they are rebased onto the current session's ID diff --git a/src/ifcviewer/SceneLoader.cpp b/src/ifcviewer/SceneLoader.cpp index 966e2d3d84..8241b64d5c 100644 --- a/src/ifcviewer/SceneLoader.cpp +++ b/src/ifcviewer/SceneLoader.cpp @@ -54,12 +54,6 @@ QString SceneLoader::displayName(uint32_t mid) const { return it == models_.end() ? QString() : it->second.display_name; } -uint64_t SceneLoader::fileSize(uint32_t mid) const { - auto it = models_.find(mid); - if (it == models_.end()) return 0; - return static_cast(QFileInfo(it->second.file_path).size()); -} - ifcopenshell::file* SceneLoader::ifcFile(uint32_t mid) const { auto it = models_.find(mid); return it == models_.end() ? nullptr : it->second.streamer->ifcFile(); @@ -124,14 +118,13 @@ void SceneLoader::startNextLoad() { emit loadStarted(model.id, model.display_name); std::string ifc_path = model.file_path.toStdString(); - uint64_t file_size = this->fileSize(model.id); uint32_t mid = loading_model_id_; // Sidecar read on a background thread so the UI stays responsive. joinSidecarThread(); - sidecar_read_thread_ = std::thread([this, ifc_path, file_size, mid]() { + sidecar_read_thread_ = std::thread([this, ifc_path, mid]() { QElapsedTimer rt; rt.start(); - auto cached = readSidecar(ifc_path, file_size); + auto cached = readSidecar(ifc_path); qDebug(" Sidecar read: %lld ms (%s)", rt.elapsed(), ifc_path.c_str()); auto result = std::make_shared>(std::move(cached)); QMetaObject::invokeMethod(this, [this, mid, result]() { diff --git a/src/ifcviewer/SceneLoader.h b/src/ifcviewer/SceneLoader.h index ad970c8468..6d63e144ef 100644 --- a/src/ifcviewer/SceneLoader.h +++ b/src/ifcviewer/SceneLoader.h @@ -63,7 +63,6 @@ public: QString filePath(uint32_t mid) const; QString displayName(uint32_t mid) const; - uint64_t fileSize(uint32_t mid) const; ifcopenshell::file* ifcFile(uint32_t mid) const; signals: diff --git a/src/ifcviewer/SidecarCache.cpp b/src/ifcviewer/SidecarCache.cpp index 171bf4bda6..12077e9c4a 100644 --- a/src/ifcviewer/SidecarCache.cpp +++ b/src/ifcviewer/SidecarCache.cpp @@ -17,16 +17,12 @@ * * ********************************************************************************/ -// v6 layout (all multi-byte fields native-endian; endianness marker in header). -// Same sequence as v5; the only change is that vertex data is now raw bytes -// at the 16 B/vertex quantized layout (see InstancedGeometry.h). -// +// v8 layout (all multi-byte fields native-endian; endianness marker in header). // // SidecarHeader (16 bytes) -// uint64_t source_file_size // // uint32_t num_vertex_bytes -// uint8_t[] vertex data (16 B/vertex: pos u16x3 + pad2 + oct-normal i16x2 + rgba8) +// uint8_t[] vertex data (12 B/vertex: pos u16x3 + oct-normal i8x2 + rgba8) // uint32_t num_indices // uint32_t[] index data (mesh-local indices; base_vertex applied at draw time) // @@ -53,8 +49,20 @@ struct SidecarHeader { uint32_t reserved; }; +// foo.ifc -> foo.ifcview +// foo.ifcdb/ -> foo.ifcview +// foo.ifcdb -> foo.ifcview +// foo (no ext) -> foo.ifcview static std::string sidecarPath(const std::string& ifc_path) { - return ifc_path + ".ifcview"; + std::string p = ifc_path; + while (!p.empty() && (p.back() == '/' || p.back() == '\\')) p.pop_back(); + auto slash = p.find_last_of("/\\"); + auto dot = p.find_last_of('.'); + std::string stem = (dot != std::string::npos && + (slash == std::string::npos || dot > slash)) + ? p.substr(0, dot) + : p; + return stem + ".ifcview"; } template @@ -74,16 +82,13 @@ static bool readVec(FILE* f, std::vector& v) { return true; } -bool writeSidecar(const std::string& ifc_path, - const SidecarData& data, - uint64_t ifc_file_size) { +bool writeSidecar(const std::string& ifc_path, const SidecarData& data) { std::string path = sidecarPath(ifc_path); FILE* f = fopen(path.c_str(), "wb"); if (!f) return false; SidecarHeader hdr = { SIDECAR_MAGIC, SIDECAR_VERSION, SIDECAR_ENDIAN, 0 }; if (fwrite(&hdr, sizeof(hdr), 1, f) != 1) { fclose(f); return false; } - if (fwrite(&ifc_file_size, 8, 1, f) != 1) { fclose(f); return false; } if (!writeVec(f, data.vertices)) { fclose(f); return false; } if (!writeVec(f, data.indices)) { fclose(f); return false; } @@ -101,8 +106,7 @@ bool writeSidecar(const std::string& ifc_path, return true; } -std::optional readSidecar(const std::string& ifc_path, - uint64_t ifc_file_size) { +std::optional readSidecar(const std::string& ifc_path) { std::string path = sidecarPath(ifc_path); FILE* f = fopen(path.c_str(), "rb"); if (!f) return std::nullopt; @@ -115,10 +119,6 @@ std::optional readSidecar(const std::string& ifc_path, hdr.version != SIDECAR_VERSION || hdr.endian != SIDECAR_ENDIAN) return fail(); - uint64_t stored_size; - if (fread(&stored_size, 8, 1, f) != 1) return fail(); - if (stored_size != ifc_file_size) return fail(); - SidecarData data; if (!readVec(f, data.vertices)) return fail(); if (!readVec(f, data.indices)) return fail(); diff --git a/src/ifcviewer/SidecarCache.h b/src/ifcviewer/SidecarCache.h index 57d8934ad8..47411e5d5f 100644 --- a/src/ifcviewer/SidecarCache.h +++ b/src/ifcviewer/SidecarCache.h @@ -41,7 +41,11 @@ static constexpr uint32_t SIDECAR_MAGIC = 0x49465657; // "IFVW" // color u8x4). Dequant basis is per-mesh MeshInfo.local_aabb_min/max. // v7 = VBO vertices shrunk to 12 B/vertex (normal oct i8x2 replaces i16x2, // eliminating 2-byte pad + saving 2 bytes on normal). -static constexpr uint32_t SIDECAR_VERSION = 7; +// v8 = source_file_size field dropped from header. Sidecar is keyed purely +// on path stem (foo.ifc and foo.ifcdb/ both map to foo.ifcview) so the +// same cache serves either source format. Staleness is user-managed +// (delete the sidecar to force a rebuild). +static constexpr uint32_t SIDECAR_VERSION = 8; static constexpr uint32_t SIDECAR_ENDIAN = 0x01020304; // Fixed-size element record. Strings are stored as (offset, length) pairs @@ -76,12 +80,11 @@ struct SidecarData { std::string string_table; }; -// v4 writer/reader are stubbed for Commit A — no disk I/O happens. -bool writeSidecar(const std::string& ifc_path, - const SidecarData& data, - uint64_t ifc_file_size); +// Sidecar is keyed on the path stem: foo.ifc and foo.ifcdb/ both resolve to +// foo.ifcview alongside the source. No staleness check — callers delete the +// file to invalidate. +bool writeSidecar(const std::string& ifc_path, const SidecarData& data); -std::optional readSidecar(const std::string& ifc_path, - uint64_t ifc_file_size); +std::optional readSidecar(const std::string& ifc_path); #endif // SIDECARCACHE_H