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);
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");
}
+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 |
| `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
+2 -9
View File
@@ -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<uint64_t>(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::optional<SidecarData>>(std::move(cached));
QMetaObject::invokeMethod(this, [this, mid, result]() {
-1
View File
@@ -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:
+17 -17
View File
@@ -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<typename T>
@@ -74,16 +82,13 @@ static bool readVec(FILE* f, std::vector<T>& 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<SidecarData> readSidecar(const std::string& ifc_path,
uint64_t ifc_file_size) {
std::optional<SidecarData> 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<SidecarData> 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();
+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.
// 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<SidecarData> readSidecar(const std::string& ifc_path,
uint64_t ifc_file_size);
std::optional<SidecarData> readSidecar(const std::string& ifc_path);
#endif // SIDECARCACHE_H