ifcviewer: key sidecar on path stem, drop staleness check

Previously readSidecar/writeSidecar were keyed on (path, file_size) with
staleness rejected at read time.  Switch to pure path-stem keying: foo.ifc
and foo.ifcdb/ both resolve to foo.ifcview, so the same cache serves either
source format.  Staleness is user-managed (delete the sidecar to force a
rebuild), which also lets sidecars be copied or moved independently of the
source.

v8 header drops the source_file_size field.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-04-22 11:58:04 +10:00
parent f898089b27
commit 4f929e90a7
6 changed files with 36 additions and 40 deletions
+1 -1
View File
@@ -295,7 +295,7 @@ void MainWindow::writeSidecarForModel(uint32_t mid) {
viewport_->applyLodExtension(mid, sd); viewport_->applyLodExtension(mid, sd);
QElapsedTimer t; t.start(); 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"); qDebug(" Sidecar write: %lld ms (%s)", t.elapsed(), ok ? "ok" : "FAILED");
} }
+6 -5
View File
@@ -116,7 +116,7 @@ engine with a Qt6 interface and OpenGL 4.5 rendering.
| `InstancedGeometry.h` | Shared structs: `MeshInfo`, `InstanceCpu`, `InstanceGpu`, chunk records | | `InstancedGeometry.h` | Shared structs: `MeshInfo`, `InstanceCpu`, `InstanceGpu`, chunk records |
| `BvhAccel.h/cpp` | Median-split BVH builder; operates on instance world-AABBs | | `BvhAccel.h/cpp` | Median-split BVH builder; operates on instance world-AABBs |
| `LodBuilder.h/cpp` | Post-stream decimation of unique meshes via meshoptimizer (`simplifySloppy`) | | `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) | | `AppSettings.h/cpp` | Persisted preferences (geometry library, stats overlay, backface culling) |
| `SettingsWindow.h/cpp` | Settings dialog | | `SettingsWindow.h/cpp` | Settings dialog |
| `CMakeLists.txt` | Build configuration | | `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 Depth 64 is enough for billions of items on any balanced tree. The stack
is on the C++ stack, zero per-frame allocation. 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. Raw memory dump, Blender-`.blend`-style — no serialisation, no parsing.
Stores everything needed to skip the `IfcGeom::Iterator` pass: Stores everything needed to skip the `IfcGeom::Iterator` pass:
``` ```
SidecarHeader (magic "IFVW", version, endian, ...) 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 + uint8_t[] vertex data (12 B/vert quantized; per-mesh basis in MeshInfo)
uint32_t + uint32_t[] index data (mesh-local) uint32_t + uint32_t[] index data (mesh-local)
uint32_t + MeshInfo[] per-unique-mesh metadata (56 B each, incl. LOD1 slice) 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 uint32_t + char[] string table
``` ```
Staleness check: `source_file_size` vs actual file size. Mismatched → Sidecar path is the source stem + `.ifcview``foo.ifc` and `foo.ifcdb/`
reject and rebuild. Endianness marker rejects cross-arch caches. 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 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 that wrote them. On load they are rebased onto the current session's ID
+2 -9
View File
@@ -54,12 +54,6 @@ QString SceneLoader::displayName(uint32_t mid) const {
return it == models_.end() ? QString() : it->second.display_name; 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<uint64_t>(QFileInfo(it->second.file_path).size());
}
ifcopenshell::file* SceneLoader::ifcFile(uint32_t mid) const { ifcopenshell::file* SceneLoader::ifcFile(uint32_t mid) const {
auto it = models_.find(mid); auto it = models_.find(mid);
return it == models_.end() ? nullptr : it->second.streamer->ifcFile(); return it == models_.end() ? nullptr : it->second.streamer->ifcFile();
@@ -124,14 +118,13 @@ void SceneLoader::startNextLoad() {
emit loadStarted(model.id, model.display_name); emit loadStarted(model.id, model.display_name);
std::string ifc_path = model.file_path.toStdString(); std::string ifc_path = model.file_path.toStdString();
uint64_t file_size = this->fileSize(model.id);
uint32_t mid = loading_model_id_; uint32_t mid = loading_model_id_;
// Sidecar read on a background thread so the UI stays responsive. // Sidecar read on a background thread so the UI stays responsive.
joinSidecarThread(); 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(); 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()); qDebug(" Sidecar read: %lld ms (%s)", rt.elapsed(), ifc_path.c_str());
auto result = std::make_shared<std::optional<SidecarData>>(std::move(cached)); auto result = std::make_shared<std::optional<SidecarData>>(std::move(cached));
QMetaObject::invokeMethod(this, [this, mid, result]() { QMetaObject::invokeMethod(this, [this, mid, result]() {
-1
View File
@@ -63,7 +63,6 @@ public:
QString filePath(uint32_t mid) const; QString filePath(uint32_t mid) const;
QString displayName(uint32_t mid) const; QString displayName(uint32_t mid) const;
uint64_t fileSize(uint32_t mid) const;
ifcopenshell::file* ifcFile(uint32_t mid) const; ifcopenshell::file* ifcFile(uint32_t mid) const;
signals: signals:
+17 -17
View File
@@ -17,16 +17,12 @@
* * * *
********************************************************************************/ ********************************************************************************/
// v6 layout (all multi-byte fields native-endian; endianness marker in header). // v8 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).
//
// //
// SidecarHeader (16 bytes) // SidecarHeader (16 bytes)
// uint64_t source_file_size
// //
// uint32_t num_vertex_bytes // 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 num_indices
// uint32_t[] index data (mesh-local indices; base_vertex applied at draw time) // uint32_t[] index data (mesh-local indices; base_vertex applied at draw time)
// //
@@ -53,8 +49,20 @@ struct SidecarHeader {
uint32_t reserved; 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) { 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<typename T> template<typename T>
@@ -74,16 +82,13 @@ static bool readVec(FILE* f, std::vector<T>& v) {
return true; return true;
} }
bool writeSidecar(const std::string& ifc_path, bool writeSidecar(const std::string& ifc_path, const SidecarData& data) {
const SidecarData& data,
uint64_t ifc_file_size) {
std::string path = sidecarPath(ifc_path); std::string path = sidecarPath(ifc_path);
FILE* f = fopen(path.c_str(), "wb"); FILE* f = fopen(path.c_str(), "wb");
if (!f) return false; if (!f) return false;
SidecarHeader hdr = { SIDECAR_MAGIC, SIDECAR_VERSION, SIDECAR_ENDIAN, 0 }; SidecarHeader hdr = { SIDECAR_MAGIC, SIDECAR_VERSION, SIDECAR_ENDIAN, 0 };
if (fwrite(&hdr, sizeof(hdr), 1, f) != 1) { fclose(f); return false; } 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.vertices)) { fclose(f); return false; }
if (!writeVec(f, data.indices)) { 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; return true;
} }
std::optional<SidecarData> readSidecar(const std::string& ifc_path, std::optional<SidecarData> readSidecar(const std::string& ifc_path) {
uint64_t ifc_file_size) {
std::string path = sidecarPath(ifc_path); std::string path = sidecarPath(ifc_path);
FILE* f = fopen(path.c_str(), "rb"); FILE* f = fopen(path.c_str(), "rb");
if (!f) return std::nullopt; if (!f) return std::nullopt;
@@ -115,10 +119,6 @@ std::optional<SidecarData> readSidecar(const std::string& ifc_path,
hdr.version != SIDECAR_VERSION || hdr.version != SIDECAR_VERSION ||
hdr.endian != SIDECAR_ENDIAN) return fail(); 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; SidecarData data;
if (!readVec(f, data.vertices)) return fail(); if (!readVec(f, data.vertices)) return fail();
if (!readVec(f, data.indices)) return fail(); if (!readVec(f, data.indices)) return fail();
+10 -7
View File
@@ -41,7 +41,11 @@ static constexpr uint32_t SIDECAR_MAGIC = 0x49465657; // "IFVW"
// color u8x4). Dequant basis is per-mesh MeshInfo.local_aabb_min/max. // 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, // v7 = VBO vertices shrunk to 12 B/vertex (normal oct i8x2 replaces i16x2,
// eliminating 2-byte pad + saving 2 bytes on normal). // 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; static constexpr uint32_t SIDECAR_ENDIAN = 0x01020304;
// Fixed-size element record. Strings are stored as (offset, length) pairs // Fixed-size element record. Strings are stored as (offset, length) pairs
@@ -76,12 +80,11 @@ struct SidecarData {
std::string string_table; std::string string_table;
}; };
// v4 writer/reader are stubbed for Commit A — no disk I/O happens. // Sidecar is keyed on the path stem: foo.ifc and foo.ifcdb/ both resolve to
bool writeSidecar(const std::string& ifc_path, // foo.ifcview alongside the source. No staleness check — callers delete the
const SidecarData& data, // file to invalidate.
uint64_t ifc_file_size); bool writeSidecar(const std::string& ifc_path, const SidecarData& data);
std::optional<SidecarData> readSidecar(const std::string& ifc_path, std::optional<SidecarData> readSidecar(const std::string& ifc_path);
uint64_t ifc_file_size);
#endif // SIDECARCACHE_H #endif // SIDECARCACHE_H