diff --git a/src/ifcviewer-wgpu/WgpuStreamingLoader.cpp b/src/ifcviewer-wgpu/WgpuStreamingLoader.cpp
new file mode 100644
index 0000000000..890f4da7af
--- /dev/null
+++ b/src/ifcviewer-wgpu/WgpuStreamingLoader.cpp
@@ -0,0 +1,170 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+// v13 sidecar layout (matched against SidecarCache.cpp):
+//
+// SidecarHeader (12 bytes)
+// uint32 num_vertex_bytes
+// uint8[num_vertex_bytes] vertex data <-- streaming skips
+// uint32 num_indices
+// uint32[num_indices] index data <-- streaming skips
+// uint32 num_meshes + MeshInfo[] <-- streaming reads
+// uint32 num_instances + InstanceCpu[] <-- streaming reads
+// uint32 has_coord_op + double[16] + 2× double <-- streaming reads
+// uint32 num_elements + PackedElementInfo[] <-- streaming reads
+// uint32 string_table_bytes + char[] <-- streaming reads
+//
+// Streaming reader returns offsets to the two skipped sections so chunks
+// can be range-read on demand. File handle is closed before return.
+
+#include "WgpuStreamingLoader.h"
+
+#include
+
+namespace {
+
+struct SidecarHeaderRaw {
+ uint32_t magic;
+ uint32_t version;
+ uint32_t endian;
+};
+
+template
+bool readVec(FILE* f, std::vector& v) {
+ uint32_t n;
+ if (std::fread(&n, 4, 1, f) != 1) return false;
+ v.resize(n);
+ if (n > 0 && std::fread(v.data(), sizeof(T), n, f) != n) return false;
+ return true;
+}
+
+std::string sidecarPath(const std::string& ifc_path) {
+ 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";
+}
+
+} // namespace
+
+std::optional readSidecarMetadataOnly(const std::string& ifc_path) {
+ const std::string path = sidecarPath(ifc_path);
+ FILE* f = std::fopen(path.c_str(), "rb");
+ if (!f) return std::nullopt;
+
+ auto fail = [&]() -> std::optional {
+ std::fclose(f);
+ return std::nullopt;
+ };
+
+ SidecarHeaderRaw hdr;
+ if (std::fread(&hdr, sizeof(hdr), 1, f) != 1) return fail();
+ if (hdr.magic != SIDECAR_MAGIC) return fail();
+ if (hdr.version != SIDECAR_VERSION) return fail();
+ if (hdr.endian != SIDECAR_ENDIAN) return fail();
+
+ StreamingSidecar out;
+ out.file_path = path;
+
+ // Vertex section: read count, record offset of data, seek past.
+ uint32_t num_vertex_bytes = 0;
+ if (std::fread(&num_vertex_bytes, 4, 1, f) != 1) return fail();
+ out.vertex_section_offset = uint64_t(std::ftell(f));
+ out.vertex_total_bytes = num_vertex_bytes;
+ if (std::fseek(f, long(num_vertex_bytes), SEEK_CUR) != 0) return fail();
+
+ // Index section: same dance, in u32 units.
+ uint32_t num_indices = 0;
+ if (std::fread(&num_indices, 4, 1, f) != 1) return fail();
+ out.index_section_offset = uint64_t(std::ftell(f));
+ out.index_total_count = num_indices;
+ if (std::fseek(f, long(num_indices) * 4, SEEK_CUR) != 0) return fail();
+
+ // Mesh dict + instance dict — small, load into meta.
+ if (!readVec(f, out.meta.meshes)) return fail();
+ if (!readVec(f, out.meta.instances)) return fail();
+
+ // v11 georef block (148 bytes total).
+ if (std::fread(&out.meta.has_coordinate_operation, 4, 1, f) != 1) return fail();
+ if (std::fread(out.meta.coordinate_operation_meters,
+ sizeof(double), 16, f) != 16) return fail();
+ if (std::fread(&out.meta.project_length_to_meters,
+ sizeof(double), 1, f) != 1) return fail();
+ if (std::fread(&out.meta.map_unit_to_meters,
+ sizeof(double), 1, f) != 1) return fail();
+
+ // Element table + string table.
+ if (!readVec(f, out.meta.elements)) return fail();
+ uint32_t stbl_len = 0;
+ if (std::fread(&stbl_len, 4, 1, f) != 1) return fail();
+ out.meta.string_table.resize(stbl_len);
+ if (stbl_len > 0 &&
+ std::fread(out.meta.string_table.data(), 1, stbl_len, f) != stbl_len)
+ return fail();
+
+ std::fclose(f);
+ return out;
+}
+
+bool readSidecarVertexChunk(const std::string& ifc_path,
+ uint64_t vertex_section_offset,
+ uint64_t chunk_byte_offset,
+ uint64_t chunk_byte_size,
+ std::vector& out_bytes) {
+ if (chunk_byte_size == 0) { out_bytes.clear(); return true; }
+
+ const std::string path = sidecarPath(ifc_path);
+ FILE* f = std::fopen(path.c_str(), "rb");
+ if (!f) return false;
+ if (std::fseek(f, long(vertex_section_offset + chunk_byte_offset), SEEK_SET) != 0) {
+ std::fclose(f);
+ return false;
+ }
+ out_bytes.resize(size_t(chunk_byte_size));
+ const size_t got = std::fread(out_bytes.data(), 1, size_t(chunk_byte_size), f);
+ std::fclose(f);
+ return got == size_t(chunk_byte_size);
+}
+
+bool readSidecarIndexChunk(const std::string& ifc_path,
+ uint64_t index_section_offset,
+ uint64_t chunk_first_index,
+ uint64_t chunk_index_count,
+ std::vector& out_indices) {
+ if (chunk_index_count == 0) { out_indices.clear(); return true; }
+
+ const std::string path = sidecarPath(ifc_path);
+ FILE* f = std::fopen(path.c_str(), "rb");
+ if (!f) return false;
+ const uint64_t byte_offset = index_section_offset + chunk_first_index * 4u;
+ if (std::fseek(f, long(byte_offset), SEEK_SET) != 0) {
+ std::fclose(f);
+ return false;
+ }
+ out_indices.resize(size_t(chunk_index_count));
+ const size_t got = std::fread(out_indices.data(), sizeof(uint32_t),
+ size_t(chunk_index_count), f);
+ std::fclose(f);
+ return got == size_t(chunk_index_count);
+}
diff --git a/src/ifcviewer-wgpu/WgpuStreamingLoader.h b/src/ifcviewer-wgpu/WgpuStreamingLoader.h
new file mode 100644
index 0000000000..f21bbbe24c
--- /dev/null
+++ b/src/ifcviewer-wgpu/WgpuStreamingLoader.h
@@ -0,0 +1,89 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#ifndef WGPUSTREAMINGLOADER_H
+#define WGPUSTREAMINGLOADER_H
+
+#include "SidecarCache.h"
+
+#include
+#include
+#include
+#include
+
+// Metadata-only sidecar load — the foundation for streaming. Reads the v13
+// header + mesh dict + instance dict + georef + element table from disk but
+// *skips* the bulky vertex and index byte sections, leaving file offsets +
+// sizes for later random-access reads.
+//
+// On a typical real-scene sidecar this returns in milliseconds even when the
+// full readSidecar would block on hundreds of MB of vertex bytes. Lets the
+// renderer set up cull / instance state immediately and load vertex chunks
+// on demand as they become frustum-visible.
+//
+// Backwards-compatible with v13 sidecars on disk (the format isn't changing
+// in this step — we're just reading less of it). v14 with an explicit
+// per-chunk TOC arrives in a follow-up; this layer abstracts the chunk
+// boundaries so the upgrade is internal.
+struct StreamingSidecar {
+ // Everything except vertices + indices — same shape as SidecarData but
+ // with empty vertices / indices vectors. The renderer uses meshes /
+ // instances / georef / elements immediately.
+ SidecarData meta;
+
+ // Byte offsets in the on-disk file where the vertex and index sections
+ // start (after their 4-byte count headers). Pair with vertex_total_bytes
+ // / index_total_bytes for the section length; per-chunk reads slice
+ // arbitrary ranges within these.
+ uint64_t vertex_section_offset = 0;
+ uint64_t vertex_total_bytes = 0;
+ uint64_t index_section_offset = 0;
+ uint64_t index_total_count = 0; // u32 indices, NOT bytes
+
+ // Resolved on-disk path so subsequent chunk reads can re-open / seek.
+ std::string file_path;
+};
+
+// Read just the metadata + section offsets. Returns nullopt on any I/O or
+// version error (same failure modes as readSidecar). The file is closed
+// before return — callers re-open for per-chunk reads.
+std::optional readSidecarMetadataOnly(const std::string& ifc_path);
+
+// Read a byte range from a sidecar's vertex section. `chunk_byte_offset` is
+// RELATIVE to vertex_section_offset (i.e. 0 = first vertex byte). Returns
+// false on I/O error or out-of-range request.
+//
+// Synchronous; intended to be called from a worker thread for async
+// streaming or from the main thread for stage-1 on-demand load.
+bool readSidecarVertexChunk(const std::string& ifc_path,
+ uint64_t vertex_section_offset,
+ uint64_t chunk_byte_offset,
+ uint64_t chunk_byte_size,
+ std::vector& out_bytes);
+
+// Read a u32-index range. `chunk_first_index` is RELATIVE to the start of
+// the index section (i.e. 0 = first u32 index). `chunk_index_count` is in
+// indices (multiply by 4 internally).
+bool readSidecarIndexChunk(const std::string& ifc_path,
+ uint64_t index_section_offset,
+ uint64_t chunk_first_index,
+ uint64_t chunk_index_count,
+ std::vector& out_indices);
+
+#endif // WGPUSTREAMINGLOADER_H