diff --git a/src/ifcviewer/ViewportCore.cpp b/src/ifcviewer/ViewportCore.cpp index b9322b7eaf..ee07e5754c 100644 --- a/src/ifcviewer/ViewportCore.cpp +++ b/src/ifcviewer/ViewportCore.cpp @@ -2374,3 +2374,507 @@ void ViewportCore::cullModelCpuUpload(ModelGpuData& m) { wgpuQueueWriteBuffer(queue_, c.per_chunk_uniform, 0, um, sizeof(um)); } } + +// =========================================================================== +// Sidecar / direct load (#84-q): applyCachedModel + uploadMeshChunk + +// uploadInstanceChunk + finalizeModel +// =========================================================================== + +#include "ChunkPlanner.h" +#include "VertexQuantization.h" + +namespace { + +// Allocate a wgpu buffer of `size_bytes` with the given usage, and upload +// `data` into it via the queue. Returns nullptr when size_bytes == 0 +// (wgpu rejects zero-sized buffer creation). `label` is informational; +// it shows up in validation messages when something goes wrong. +WGPUBuffer createBufferWithData(WGPUDevice device, WGPUQueue queue, + const void* data, std::size_t size_bytes, + WGPUBufferUsage usage, + const char* label) { + if (size_bytes == 0) return nullptr; + + WGPUBufferDescriptor desc = {}; + desc.size = std::uint64_t(size_bytes); + desc.usage = usage | WGPUBufferUsage_CopyDst; + if (label) { + desc.label.data = label; + desc.label.length = std::strlen(label); + } + WGPUBuffer buf = wgpuDeviceCreateBuffer(device, &desc); + if (buf && data) { + wgpuQueueWriteBuffer(queue, buf, 0, data, size_bytes); + } + return buf; +} + +// Look up (or create) the direct-load staging entry for a given model. +// Holds a unique_ptr so address stability is preserved as the map grows. +SidecarData& getOrCreateDirectStaging( + std::unordered_map>& staging, + std::uint32_t model_id) { + auto it = staging.find(model_id); + if (it == staging.end()) { + auto [it_new, _] = staging.emplace( + model_id, std::make_unique()); + return *it_new->second; + } + return *it->second; +} + +} // namespace + +void ViewportCore::applyCachedModel(std::uint32_t model_id, + StreamingSidecar metadata) { + if (!device_ || !queue_) { + Log::warn() << "applyCachedModel without an initialised device"; + return; + } + + // Replace any existing state for this id. + auto it = models_gpu_.find(model_id); + if (it != models_gpu_.end()) { + releaseWgpuModelGpuData(it->second, pool_); + models_gpu_.erase(it); + } + + ModelGpuData m; + m.vertex_bytes = metadata.vertex_total_bytes; + m.index_count = std::uint32_t(metadata.index_total_count); + m.mesh_count = std::uint32_t(metadata.meta.meshes.size()); + m.instance_count = std::uint32_t(metadata.meta.instances.size()); + m.streaming_file_path = metadata.file_path; + m.streaming_vertex_section_offset = metadata.vertex_section_offset; + m.streaming_index_section_offset = metadata.index_section_offset; + + // ---- Spatial chunk plan ---------------------------------------------- + // Sort meshes by 3D Morton code over centroids, then greedy-pack into + // chunks <= WGPU_CHUNK_VERTEX_BYTES_LIMIT. Each chunk's AABB ends up + // tight rather than spanning the whole model, so the distance-based + // streaming evictor can meaningfully distinguish chunks. + const std::size_t n_meshes = metadata.meta.meshes.size(); + m.mesh_chunk_idx.assign(n_meshes, 0); + m.mesh_chunk_local_base_vertex.assign(n_meshes, 0); + m.mesh_chunk_local_ebo_first_u32.assign(n_meshes, 0); + m.mesh_chunk_local_lod1_first_u32.assign(n_meshes, 0); + + std::vector mesh_cx(n_meshes, 0.0f), + mesh_cy(n_meshes, 0.0f), + mesh_cz(n_meshes, 0.0f); + std::vector mesh_inst_count(n_meshes, 0); + for (const auto& inst : metadata.meta.instances) { + if (inst.mesh_id >= n_meshes) continue; + mesh_cx[inst.mesh_id] += 0.5f * (inst.world_aabb_min[0] + inst.world_aabb_max[0]); + mesh_cy[inst.mesh_id] += 0.5f * (inst.world_aabb_min[1] + inst.world_aabb_max[1]); + mesh_cz[inst.mesh_id] += 0.5f * (inst.world_aabb_min[2] + inst.world_aabb_max[2]); + ++mesh_inst_count[inst.mesh_id]; + } + for (std::size_t i = 0; i < n_meshes; ++i) { + if (mesh_inst_count[i] > 0) { + const float inv = 1.0f / float(mesh_inst_count[i]); + mesh_cx[i] *= inv; mesh_cy[i] *= inv; mesh_cz[i] *= inv; + } + } + + std::vector> chunk_mesh_ids; + std::vector instance_to_chunk; + instance_to_chunk.assign(metadata.meta.instances.size(), 0); + { + std::vector sorted_mesh_ids = ChunkPlanner::sortMeshIdsByMorton( + n_meshes, mesh_cx, mesh_cy, mesh_cz, mesh_inst_count); + std::vector mesh_vertex_count; + mesh_vertex_count.reserve(n_meshes); + for (std::size_t i = 0; i < n_meshes; ++i) { + mesh_vertex_count.push_back(metadata.meta.meshes[i].vertex_count); + } + chunk_mesh_ids = ChunkPlanner::greedyPackChunks( + sorted_mesh_ids, mesh_vertex_count, + INSTANCED_VERTEX_STRIDE_BYTES, + WGPU_CHUNK_VERTEX_BYTES_LIMIT); + std::vector mesh_to_chunk(n_meshes, 0); + for (std::size_t ci = 0; ci < chunk_mesh_ids.size(); ++ci) { + for (std::uint32_t mi : chunk_mesh_ids[ci]) mesh_to_chunk[mi] = std::uint32_t(ci); + } + for (std::size_t i = 0; i < metadata.meta.instances.size(); ++i) { + const std::uint32_t mi = metadata.meta.instances[i].mesh_id; + if (mi < n_meshes) instance_to_chunk[i] = mesh_to_chunk[mi]; + } + } + + std::vector chunk_instance_count(chunk_mesh_ids.size(), 0); + for (std::size_t i = 0; i < instance_to_chunk.size(); ++i) { + const std::uint32_t ci = instance_to_chunk[i]; + if (ci < chunk_instance_count.size()) ++chunk_instance_count[ci]; + } + + // ---- Allocate per-chunk state. NO pool slices yet (chunks are + // non-resident); the per-frame loader brings them in as cull marks + // them visible. + m.chunks.resize(chunk_mesh_ids.size()); + struct MeshLocal { + std::uint32_t base_vertex; + std::uint32_t ebo_first; + std::uint32_t lod1_first; + }; + std::vector> + chunk_mesh_offsets(chunk_mesh_ids.size()); + for (std::size_t ci = 0; ci < chunk_mesh_ids.size(); ++ci) { + ModelGpuData::Chunk& c = m.chunks[ci]; + c.mesh_ids = std::move(chunk_mesh_ids[ci]); + c.is_resident = false; + + std::uint32_t chunk_local_v = 0; + std::uint32_t chunk_local_i = 0; + for (std::uint32_t mi : c.mesh_ids) { + const MeshInfo& mesh = metadata.meta.meshes[mi]; + m.mesh_chunk_idx[mi] = std::uint32_t(ci); + m.mesh_chunk_local_base_vertex[mi] = chunk_local_v; + m.mesh_chunk_local_ebo_first_u32[mi] = chunk_local_i; + chunk_mesh_offsets[ci][mi] = MeshLocal{chunk_local_v, chunk_local_i, 0}; + chunk_local_v += mesh.vertex_count; + chunk_local_i += mesh.index_count; + } + std::uint32_t chunk_local_lod1 = 0; + for (std::uint32_t mi : c.mesh_ids) { + const MeshInfo& mesh = metadata.meta.meshes[mi]; + if (mesh.lod1_index_count == 0) continue; + m.mesh_chunk_local_lod1_first_u32[mi] = chunk_local_i + chunk_local_lod1; + chunk_mesh_offsets[ci][mi].lod1_first = chunk_local_i + chunk_local_lod1; + chunk_local_lod1 += mesh.lod1_index_count; + } + c.vertex_count = chunk_local_v; + c.vertex_byte_size = std::uint64_t(chunk_local_v) * INSTANCED_VERTEX_STRIDE_BYTES; + c.index_count = chunk_local_i + chunk_local_lod1; + c.lod1_index_count = chunk_local_lod1; + + // Small per-chunk buffers, allocated upfront so cull can write into + // them. visible_draws_buffer cap = chunk's instance count. + const std::size_t chunk_inst = std::max(chunk_instance_count[ci], 1); + const std::size_t draws_bytes = chunk_inst * sizeof(ModelGpuData::VisibleDrawGpu); + const std::size_t ps_bytes = (chunk_inst + 1) * sizeof(std::uint32_t); + + WGPUBufferDescriptor vd_desc = {}; + vd_desc.size = std::max(draws_bytes, 16); + vd_desc.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst; + vd_desc.label = svFromCStr("model.chunk.visible_draws"); + c.visible_draws_buffer = wgpuDeviceCreateBuffer(device_, &vd_desc); + c.visible_draws_capacity = chunk_inst; + m.vram_bytes_ssbo += vd_desc.size; + + WGPUBufferDescriptor ps_desc = {}; + ps_desc.size = std::max(ps_bytes, 16); + ps_desc.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst; + ps_desc.label = svFromCStr("model.chunk.prefix_sums"); + c.prefix_sums_buffer = wgpuDeviceCreateBuffer(device_, &ps_desc); + c.prefix_sums_capacity = chunk_inst + 1; + m.vram_bytes_ssbo += ps_desc.size; + + WGPUBufferDescriptor mu_desc = {}; + mu_desc.size = 16; + mu_desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst; + mu_desc.label = svFromCStr("model.chunk.uniform"); + c.per_chunk_uniform = wgpuDeviceCreateBuffer(device_, &mu_desc); + m.vram_bytes_ssbo += 16; + + c.visible_draws_scratch.reserve(chunk_inst); + c.prefix_sums_scratch.reserve(chunk_inst + 1); + } + + // Index section is NOT loaded upfront. Each chunk's index slice is + // range-read alongside its vertex bytes in loadChunkBytesAndUploadGpu. + + // MeshGpu storage (per-mesh quant basis). + std::vector mesh_gpu; + mesh_gpu.reserve(metadata.meta.meshes.size()); + for (const auto& mi : metadata.meta.meshes) { + MeshGpu mg = {}; + mg.aabb_min[0] = mi.local_aabb_min[0]; + mg.aabb_min[1] = mi.local_aabb_min[1]; + mg.aabb_min[2] = mi.local_aabb_min[2]; + mg.aabb_max[0] = mi.local_aabb_max[0]; + mg.aabb_max[1] = mi.local_aabb_max[1]; + mg.aabb_max[2] = mi.local_aabb_max[2]; + mesh_gpu.push_back(mg); + } + const std::size_t mesh_storage_bytes = mesh_gpu.size() * sizeof(MeshGpu); + m.mesh_storage = createBufferWithData( + device_, queue_, + mesh_gpu.data(), mesh_storage_bytes, + WGPUBufferUsage_Storage, + "model.mesh_storage"); + m.vram_bytes_ssbo += mesh_storage_bytes; + + // InstanceGpu storage. Rebase object_ids globally. + const std::uint32_t object_id_base = next_object_id_; + std::uint32_t max_local_id = 0; + std::vector inst_gpu; + inst_gpu.reserve(metadata.meta.instances.size()); + for (auto& ic : metadata.meta.instances) { + if (ic.object_id > max_local_id) max_local_id = ic.object_id; + ic.object_id = object_id_base + ic.object_id; + InstanceGpu ig = {}; + std::memcpy(ig.transform, ic.transform, sizeof(ig.transform)); + ig.object_id = ic.object_id; + ig.color_override_rgba8 = ic.color_override_rgba8; + ig.mesh_id = ic.mesh_id; + inst_gpu.push_back(ig); + } + next_object_id_ = object_id_base + max_local_id + 1; + const std::size_t inst_storage_bytes = inst_gpu.size() * sizeof(InstanceGpu); + m.instance_storage = createBufferWithData( + device_, queue_, + inst_gpu.data(), inst_storage_bytes, + WGPUBufferUsage_Storage, + "model.instance_storage"); + m.vram_bytes_ssbo += inst_storage_bytes; + + // Hand off CPU mirrors. + m.meshes = std::move(metadata.meta.meshes); + m.instances = std::move(metadata.meta.instances); + + // Streaming defers per-mesh vertex data until the owning chunk is + // loaded. Both volumes + Area-tool CPU shadow fill in per-chunk + // inside applyStreamedChunk as the bytes arrive. + m.mesh_local_volumes.assign(m.meshes.size(), 0.0); + m.mesh_triangles_cache.assign(m.meshes.size(), ModelGpuData::MeshTriangles{}); + m.mesh_has_alpha.assign(m.meshes.size(), std::uint8_t(0)); + + // object_id → instance index lookup. Volume tool reads it on every + // selection mutation; per-pick latency stays O(K) instead of O(K*N). + m.object_id_to_instance.clear(); + m.object_id_to_instance.reserve(m.instances.size()); + for (std::uint32_t i = 0; i < std::uint32_t(m.instances.size()); ++i) { + m.object_id_to_instance.emplace(m.instances[i].object_id, i); + } + + // Per-chunk world AABBs + instance-id lists from instance_to_chunk. + for (std::size_t ci = 0; ci < m.chunks.size(); ++ci) { + m.chunks[ci].instance_ids.reserve(m.instances.size() / m.chunks.size() + 4); + } + for (std::uint32_t inst_idx = 0; inst_idx < std::uint32_t(m.instances.size()); ++inst_idx) { + const auto& inst = m.instances[inst_idx]; + const std::uint32_t ci = instance_to_chunk[inst_idx]; + if (ci >= m.chunks.size()) continue; + auto& c = m.chunks[ci]; + for (int a = 0; a < 3; ++a) { + c.aabb_min[a] = std::min(c.aabb_min[a], inst.world_aabb_min[a]); + c.aabb_max[a] = std::max(c.aabb_max[a], inst.world_aabb_max[a]); + } + c.instance_ids.push_back(inst_idx); + } + + // Populate per-instance arrays from the per-chunk per-mesh offsets + // computed during chunk construction. + { + const std::size_t n_inst = m.instances.size(); + m.instance_chunk_idx.assign(n_inst, 0); + m.instance_base_vertex.assign(n_inst, 0); + m.instance_ebo_first_u32.assign(n_inst, 0); + m.instance_lod1_first_u32.assign(n_inst, 0); + for (std::size_t i = 0; i < n_inst; ++i) { + const std::uint32_t ci = instance_to_chunk[i]; + const std::uint32_t mi = m.instances[i].mesh_id; + if (ci >= chunk_mesh_offsets.size()) continue; + auto it_off = chunk_mesh_offsets[ci].find(mi); + if (it_off == chunk_mesh_offsets[ci].end()) continue; + m.instance_chunk_idx[i] = ci; + m.instance_base_vertex[i] = it_off->second.base_vertex; + m.instance_ebo_first_u32[i] = it_off->second.ebo_first; + m.instance_lod1_first_u32[i] = it_off->second.lod1_first; + } + } + + auto [inserted, _] = models_gpu_.emplace(model_id, std::move(m)); + ModelGpuData& mref = inserted->second; + + Log::info() + << "[wgpu stream] applyCachedModel mid=" << model_id + << " verts=" << mref.vertex_bytes << "B (deferred)" + << " idx=" << mref.index_count + << " meshes=" << mref.mesh_count + << " instances=" << mref.instance_count + << " chunks=" << mref.chunks.size(); + + if (!initial_view_applied_) { + viewAll(); + initial_view_applied_ = true; + } + ensureSelectionFlagsBuffer(); + host_->requestFrame(); +} + +void ViewportCore::uploadMeshChunk(const MeshChunk& chunk) { + if (chunk.vertices.empty() || chunk.indices.empty()) return; + SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, chunk.model_id); + + // Streamer format: 7 floats / vertex (pos3 + normal3 + color-as-float). + // Same quantisation as SidecarBuilder::onMeshReady so direct-load and + // sidecar-load produce byte-identical GPU buffers. + const std::size_t n_verts = chunk.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS; + + float bmin[3] = { std::numeric_limits::infinity(), + std::numeric_limits::infinity(), + std::numeric_limits::infinity() }; + float bmax[3] = { -std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + -std::numeric_limits::infinity() }; + for (std::size_t i = 0; i < n_verts; ++i) { + const float* v = chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS; + for (int a = 0; a < 3; ++a) { + if (v[a] < bmin[a]) bmin[a] = v[a]; + if (v[a] > bmax[a]) bmax[a] = v[a]; + } + } + float extent_recip[3]; + for (int a = 0; a < 3; ++a) { + const float ext = bmax[a] - bmin[a]; + extent_recip[a] = ext > 0.0f ? 1.0f / ext : 0.0f; + } + + const std::size_t vb_offset = s.vertices.size(); + s.vertices.resize(vb_offset + n_verts * INSTANCED_VERTEX_STRIDE_BYTES); + for (std::size_t i = 0; i < n_verts; ++i) { + quantizeVertex(chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS, + bmin, extent_recip, + s.vertices.data() + vb_offset + + i * INSTANCED_VERTEX_STRIDE_BYTES); + } + + const std::size_t ib_offset = s.indices.size(); + s.indices.insert(s.indices.end(), + chunk.indices.begin(), chunk.indices.end()); + + MeshInfo info{}; + info.vbo_byte_offset = std::uint32_t(vb_offset); + info.vertex_count = std::uint32_t(n_verts); + info.ebo_byte_offset = std::uint32_t(ib_offset * sizeof(std::uint32_t)); + info.index_count = std::uint32_t(chunk.indices.size()); + for (int a = 0; a < 3; ++a) { + info.local_aabb_min[a] = bmin[a]; + info.local_aabb_max[a] = bmax[a]; + } + info.first_instance = 0; + info.instance_count = 0; + info.lod1_ebo_byte_offset = 0; + info.lod1_index_count = 0; + + if (s.meshes.size() <= chunk.local_mesh_id) { + s.meshes.resize(chunk.local_mesh_id + 1); + } + s.meshes[chunk.local_mesh_id] = info; +} + +void ViewportCore::uploadInstanceChunk(const InstanceChunk& chunk) { + SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, chunk.model_id); + + InstanceCpu inst{}; + inst.mesh_id = chunk.local_mesh_id; + inst.object_id = chunk.object_id; + inst.color_override_rgba8 = chunk.color_override_rgba8; + inst.model_id = chunk.model_id; + std::memcpy(inst.placement_transformation, chunk.transform, + sizeof(inst.placement_transformation)); + for (int i = 0; i < 16; ++i) { + inst.transform[i] = float(chunk.transform[i]); + } + std::memcpy(inst.world_aabb_min, chunk.world_aabb_min, sizeof(inst.world_aabb_min)); + std::memcpy(inst.world_aabb_max, chunk.world_aabb_max, sizeof(inst.world_aabb_max)); + + s.instances.push_back(inst); +} + +void ViewportCore::finalizeModel(std::uint32_t model_id) { + auto it = pending_direct_loads_.find(model_id); + if (it == pending_direct_loads_.end()) { + Log::warn() + << "[wgpu direct] finalizeModel(" << model_id + << ") with no staged data; skipping"; + return; + } + std::unique_ptr staging_ptr = std::move(it->second); + pending_direct_loads_.erase(it); + SidecarData& s = *staging_ptr; + + if (!device_ || !queue_) { + Log::warn() << "[wgpu direct] finalizeModel without an initialised device"; + return; + } + if (s.meshes.empty() || s.instances.empty()) { + Log::info() << "[wgpu direct] finalizeModel(" << model_id + << "): empty staging (meshes=" << s.meshes.size() + << " instances=" << s.instances.size() << ")"; + return; + } + + // Build a StreamingSidecar around the staging so applyCachedModel can + // run its chunk planner over the same shape it expects from on-disk + // metadata. file_path is left empty — the streaming worker keys off + // that to skip these chunks (they're already resident after the + // applyStreamedChunk loop below). + StreamingSidecar metadata; + metadata.meta = std::move(s); + metadata.vertex_section_offset = 0; + metadata.vertex_total_bytes = metadata.meta.vertices.size(); + metadata.index_section_offset = 0; + metadata.index_total_count = metadata.meta.indices.size(); + metadata.file_path.clear(); + + std::vector raw_vertices = std::move(metadata.meta.vertices); + std::vector raw_indices = std::move(metadata.meta.indices); + + applyCachedModel(model_id, std::move(metadata)); + + auto model_it = models_gpu_.find(model_id); + if (model_it == models_gpu_.end()) { + Log::warn() + << "[wgpu direct] finalizeModel(" << model_id + << "): applyCachedModel produced no model entry"; + return; + } + ModelGpuData& m = model_it->second; + + // Gather each chunk's vertex + index bytes from the staged buffers. + std::size_t chunks_uploaded = 0; + for (std::size_t ci = 0; ci < m.chunks.size(); ++ci) { + auto& c = m.chunks[ci]; + if (c.mesh_ids.empty()) continue; + + std::vector vbytes(c.vertex_byte_size); + std::vector idx; + idx.reserve(c.index_count); + + for (std::uint32_t mi : c.mesh_ids) { + const MeshInfo& mesh = m.meshes[mi]; + const std::size_t vsz = std::size_t(mesh.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES; + if (vsz > 0) { + const std::size_t dst_off = std::size_t(m.mesh_chunk_local_base_vertex[mi]) + * INSTANCED_VERTEX_STRIDE_BYTES; + std::memcpy(vbytes.data() + dst_off, + raw_vertices.data() + mesh.vbo_byte_offset, vsz); + } + if (mesh.index_count > 0) { + const std::uint32_t* src = raw_indices.data() + + (mesh.ebo_byte_offset / sizeof(std::uint32_t)); + idx.insert(idx.end(), src, src + mesh.index_count); + } + } + + if (!applyStreamedChunk(m, ci, vbytes, idx)) { + Log::warn() + << "[wgpu direct] finalizeModel(" << model_id + << "): applyStreamedChunk failed on chunk " << ci + << " (pool OOM?)"; + continue; + } + ++chunks_uploaded; + } + + Log::info() + << "[wgpu direct] finalizeModel mid=" << model_id + << " meshes=" << m.meshes.size() + << " instances=" << m.instances.size() + << " chunks=" << chunks_uploaded << "/" << m.chunks.size() + << " verts=" << raw_vertices.size() << "B" + << " idx=" << raw_indices.size(); +} diff --git a/src/ifcviewer/ViewportCore.h b/src/ifcviewer/ViewportCore.h index 1f2b0bb21c..734165c3e7 100644 --- a/src/ifcviewer/ViewportCore.h +++ b/src/ifcviewer/ViewportCore.h @@ -39,6 +39,7 @@ #include #include +#include #include #include #include @@ -50,6 +51,8 @@ #include "ModelGpuData.h" #include "SectionPlane.h" #include "SelectionState.h" +#include "SidecarCache.h" +#include "StreamingLoader.h" #include "StreamingThread.h" #include "ViewportHost.h" #include "VisibilityState.h" @@ -300,6 +303,25 @@ public: // residency is still settling so the render loop keeps ticking. void driveStreamingLoads(); + // ---- Sidecar / direct load (#84-q) ----------------------------------- + // + // Apply a parsed sidecar's metadata + planned chunk layout to + // models_gpu_[model_id]. Builds the per-chunk small buffers + // (visible_draws / prefix_sums / per_chunk_uniform), the per-model + // mesh + instance storage SSBOs, and the spatial chunk plan; chunk + // vertex/index slices stay non-resident until the streaming loader + // brings them in. Triggers an auto-viewAll on the first model (so a + // freshly-loaded scene frames itself). + void applyCachedModel(std::uint32_t model_id, StreamingSidecar metadata); + + // Direct-load (bonsai-side) entry points. Bonsai's SceneLoader feeds + // the viewer one mesh + one instance at a time, then calls + // finalizeModel once everything's staged. The staging map lives on + // ViewportCore so both halves can share it. + void uploadMeshChunk(const MeshChunk& chunk); + void uploadInstanceChunk(const InstanceChunk& chunk); + void finalizeModel(std::uint32_t model_id); + // ---- Cull (#84-p) ----------------------------------------------------- // // Per-instance occlusion test, supplied by the caller. Wired by @@ -501,6 +523,19 @@ private: // completes. std::string pending_screenshot_path_; + // Bonsai direct-load staging map. uploadMeshChunk + + // uploadInstanceChunk append into entries keyed by model_id; the + // finalizeModel call moves the entry out, hands it to + // applyCachedModel, and uploads the chunk slices synchronously. + std::unordered_map> + pending_direct_loads_; + + // Auto-viewAll suppression. Flipped true by the first applyCachedModel + // (so a fresh scene frames itself) or by any explicit setCamera (so a + // user/bonsai-side camera write isn't overridden by the next model + // load). Lives here so applyCachedModel can read + write it. + bool initial_view_applied_ = false; + // Tool-refresh callback: fired by applyStreamedChunk when a newly- // arrived chunk filled in a mesh-local volume. ViewportWindow wires // this to its Volume-tool HUD refresh in the ctor. Null by default diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index e51a5e7563..689924fe5f 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -155,29 +155,7 @@ static QString sv(WGPUStringView s) { return QString::fromUtf8(s.data, len); } -// Allocate a wgpu buffer of `size_bytes` with the given usage, and upload -// `data` into it via the queue. Returns nullptr when size_bytes == 0 (wgpu -// rejects zero-sized buffer creation). `label` is informational; it shows up -// in validation messages when something goes wrong. -static WGPUBuffer createBufferWithData(WGPUDevice device, WGPUQueue queue, - const void* data, size_t size_bytes, - WGPUBufferUsage usage, - const char* label) { - if (size_bytes == 0) return nullptr; - - WGPUBufferDescriptor desc = {}; - desc.size = uint64_t(size_bytes); - desc.usage = usage | WGPUBufferUsage_CopyDst; - if (label) { - desc.label.data = label; - desc.label.length = std::strlen(label); - } - WGPUBuffer buf = wgpuDeviceCreateBuffer(device, &desc); - if (buf && data) { - wgpuQueueWriteBuffer(queue, buf, 0, data, size_bytes); - } - return buf; -} +// createBufferWithData moved to ViewportCore (anon namespace) (#84-q). // releaseWgpuModelGpuData moved to ViewportCore.cpp (IfcViewerCore now needs it). @@ -284,7 +262,8 @@ ViewportWindow::ViewportWindow(QWindow* parent) lod1_dbg_count_ (core_.lod1_dbg_count_), lod0_dbg_eligible_count_(core_.lod0_dbg_eligible_count_), lod0_dbg_no_lod1_count_ (core_.lod0_dbg_no_lod1_count_), - lod1_dbg_tris_saved_ (core_.lod1_dbg_tris_saved_) { + lod1_dbg_tris_saved_ (core_.lod1_dbg_tris_saved_), + initial_view_applied_ (core_.initial_view_applied_) { // wgpu doesn't need a GL context; we just need a real native window // whose backing layer matches the GPU API wgpu will drive. // @@ -506,320 +485,8 @@ uint32_t ViewportWindow::loadSidecar(const std::string& path_std) { return mid; } -void ViewportWindow::applyCachedModel(uint32_t model_id, - StreamingSidecar metadata) { - if (!device_ || !queue_) { - Log::warn() << "applyCachedModel without an initialised device"; - return; - } - - // Replace any existing state for this id. - auto it = models_gpu_.find(model_id); - if (it != models_gpu_.end()) { - releaseWgpuModelGpuData(it->second, pool_); - models_gpu_.erase(it); - } - - ModelGpuData m; - m.vertex_bytes = metadata.vertex_total_bytes; - m.index_count = uint32_t(metadata.index_total_count); - m.mesh_count = uint32_t(metadata.meta.meshes.size()); - m.instance_count = uint32_t(metadata.meta.instances.size()); - m.streaming_file_path = metadata.file_path; - m.streaming_vertex_section_offset = metadata.vertex_section_offset; - m.streaming_index_section_offset = metadata.index_section_offset; - - // ---- Spatial chunk plan ---------------------------------------------- - // Sort meshes by world-space centroid (mean of their instances' AABB - // centres), then greedy-pack into chunks ≤ WGPU_CHUNK_VERTEX_BYTES_LIMIT. - // Each chunk's AABB ends up tight rather than spanning the whole model, - // so the distance-based streaming evictor can meaningfully distinguish - // chunks. Per-mesh layout within a chunk is the spatial-sort order; - // the loader scatter-gathers from each mesh's sidecar offsets. - const size_t n_meshes = metadata.meta.meshes.size(); - m.mesh_chunk_idx.assign(n_meshes, 0); - m.mesh_chunk_local_base_vertex.assign(n_meshes, 0); - m.mesh_chunk_local_ebo_first_u32.assign(n_meshes, 0); - m.mesh_chunk_local_lod1_first_u32.assign(n_meshes, 0); - - // Per-mesh centroid = mean of its instances' world AABB centres. - // Meshes with no instances stay at (0,0,0) — they're dead weight but - // still need a chunk slot for layout consistency. - std::vector mesh_cx(n_meshes, 0.0f), - mesh_cy(n_meshes, 0.0f), - mesh_cz(n_meshes, 0.0f); - std::vector mesh_inst_count(n_meshes, 0); - for (const auto& inst : metadata.meta.instances) { - if (inst.mesh_id >= n_meshes) continue; - mesh_cx[inst.mesh_id] += 0.5f * (inst.world_aabb_min[0] + inst.world_aabb_max[0]); - mesh_cy[inst.mesh_id] += 0.5f * (inst.world_aabb_min[1] + inst.world_aabb_max[1]); - mesh_cz[inst.mesh_id] += 0.5f * (inst.world_aabb_min[2] + inst.world_aabb_max[2]); - ++mesh_inst_count[inst.mesh_id]; - } - for (size_t i = 0; i < n_meshes; ++i) { - if (mesh_inst_count[i] > 0) { - const float inv = 1.0f / float(mesh_inst_count[i]); - mesh_cx[i] *= inv; mesh_cy[i] *= inv; mesh_cz[i] *= inv; - } - } - - // Chunk planning: sort meshes by 3D Morton code over centroids, then - // greedy-pack into chunks ≤ WGPU_CHUNK_VERTEX_BYTES_LIMIT. Each mesh - // ends up in exactly one chunk. - std::vector> chunk_mesh_ids; - std::vector instance_to_chunk; - instance_to_chunk.assign(metadata.meta.instances.size(), 0); - { - std::vector sorted_mesh_ids = ChunkPlanner::sortMeshIdsByMorton( - n_meshes, mesh_cx, mesh_cy, mesh_cz, mesh_inst_count); - std::vector mesh_vertex_count; - mesh_vertex_count.reserve(n_meshes); - for (size_t i = 0; i < n_meshes; ++i) { - mesh_vertex_count.push_back(metadata.meta.meshes[i].vertex_count); - } - chunk_mesh_ids = ChunkPlanner::greedyPackChunks( - sorted_mesh_ids, mesh_vertex_count, - INSTANCED_VERTEX_STRIDE_BYTES, - WGPU_CHUNK_VERTEX_BYTES_LIMIT); - // Derive instance_to_chunk via mesh_id → chunk lookup table. - std::vector mesh_to_chunk(n_meshes, 0); - for (size_t ci = 0; ci < chunk_mesh_ids.size(); ++ci) { - for (uint32_t mi : chunk_mesh_ids[ci]) mesh_to_chunk[mi] = uint32_t(ci); - } - for (size_t i = 0; i < metadata.meta.instances.size(); ++i) { - const uint32_t mi = metadata.meta.instances[i].mesh_id; - if (mi < n_meshes) instance_to_chunk[i] = mesh_to_chunk[mi]; - } - } - - std::vector chunk_instance_count(chunk_mesh_ids.size(), 0); - for (size_t i = 0; i < instance_to_chunk.size(); ++i) { - const uint32_t ci = instance_to_chunk[i]; - if (ci < chunk_instance_count.size()) ++chunk_instance_count[ci]; - } - - // ---- Allocate per-chunk state. NO pool slices yet (chunks are - // non-resident); the per-frame loader brings them in as cull marks - // them visible. - m.chunks.resize(chunk_mesh_ids.size()); - // Per-chunk per-mesh chunk-local offsets. Built during the chunk - // construction loop, consumed by the post-loop per-instance array - // population. Under spatial bucketing the same mesh_id can land in - // multiple chunks at different offsets, so this can't be a per-mesh - // global — it has to be per-(chunk, mesh). - struct MeshLocal { uint32_t base_vertex; uint32_t ebo_first; uint32_t lod1_first; }; - std::vector> - chunk_mesh_offsets(chunk_mesh_ids.size()); - for (size_t ci = 0; ci < chunk_mesh_ids.size(); ++ci) { - ModelGpuData::Chunk& c = m.chunks[ci]; - c.mesh_ids = std::move(chunk_mesh_ids[ci]); - c.is_resident = false; // streaming - - // Walk this chunk's meshes in chunk-local layout order, computing - // each mesh's chunk-local base_vertex / ebo_first_u32 and the - // chunk's aggregate vertex/index totals. LOD1 indices (if any - // mesh has them baked) get a second pass and pack AFTER all the - // LOD0 indices in the chunk's index slice — so a single slice - // carries both LODs and cull picks per-instance by chunk-local - // u32 offset. - uint32_t chunk_local_v = 0; - uint32_t chunk_local_i = 0; - for (uint32_t mi : c.mesh_ids) { - const MeshInfo& mesh = metadata.meta.meshes[mi]; - m.mesh_chunk_idx[mi] = uint32_t(ci); - m.mesh_chunk_local_base_vertex[mi] = chunk_local_v; - m.mesh_chunk_local_ebo_first_u32[mi] = chunk_local_i; - chunk_mesh_offsets[ci][mi] = MeshLocal{chunk_local_v, chunk_local_i, 0}; - chunk_local_v += mesh.vertex_count; - chunk_local_i += mesh.index_count; - } - uint32_t chunk_local_lod1 = 0; - for (uint32_t mi : c.mesh_ids) { - const MeshInfo& mesh = metadata.meta.meshes[mi]; - if (mesh.lod1_index_count == 0) continue; - m.mesh_chunk_local_lod1_first_u32[mi] = chunk_local_i + chunk_local_lod1; - chunk_mesh_offsets[ci][mi].lod1_first = chunk_local_i + chunk_local_lod1; - chunk_local_lod1 += mesh.lod1_index_count; - } - c.vertex_count = chunk_local_v; - c.vertex_byte_size = uint64_t(chunk_local_v) * INSTANCED_VERTEX_STRIDE_BYTES; - c.index_count = chunk_local_i + chunk_local_lod1; - c.lod1_index_count = chunk_local_lod1; - - // Small per-chunk buffers, allocated upfront so cull can write into - // them. visible_draws_buffer cap = chunk's instance count (worst- - // case all visible, one entry each — LOD doesn't double-count). - const size_t chunk_inst = std::max(chunk_instance_count[ci], 1); - const size_t draws_bytes = chunk_inst * sizeof(ModelGpuData::VisibleDrawGpu); - const size_t ps_bytes = (chunk_inst + 1) * sizeof(uint32_t); - - WGPUBufferDescriptor vd_desc = {}; - vd_desc.size = std::max(draws_bytes, 16); - vd_desc.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst; - vd_desc.label = svFromCStr("model.chunk.visible_draws"); - c.visible_draws_buffer = wgpuDeviceCreateBuffer(device_, &vd_desc); - c.visible_draws_capacity = chunk_inst; - m.vram_bytes_ssbo += vd_desc.size; - - WGPUBufferDescriptor ps_desc = {}; - ps_desc.size = std::max(ps_bytes, 16); - ps_desc.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst; - ps_desc.label = svFromCStr("model.chunk.prefix_sums"); - c.prefix_sums_buffer = wgpuDeviceCreateBuffer(device_, &ps_desc); - c.prefix_sums_capacity = chunk_inst + 1; - m.vram_bytes_ssbo += ps_desc.size; - - WGPUBufferDescriptor mu_desc = {}; - mu_desc.size = 16; - mu_desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst; - mu_desc.label = svFromCStr("model.chunk.uniform"); - c.per_chunk_uniform = wgpuDeviceCreateBuffer(device_, &mu_desc); - m.vram_bytes_ssbo += 16; - - c.visible_draws_scratch.reserve(chunk_inst); - c.prefix_sums_scratch.reserve(chunk_inst + 1); - } - - // Index section is NOT loaded upfront. Each chunk's index slice will - // be range-read alongside its vertex bytes in loadChunkBytesAndUploadGpu. - // Eliminates the 1.5+ GB upfront index VRAM cost that was the binding - // OOM constraint on real scenes. - - // MeshGpu storage (per-mesh quant basis). - std::vector mesh_gpu; - mesh_gpu.reserve(metadata.meta.meshes.size()); - for (const auto& mi : metadata.meta.meshes) { - MeshGpu mg = {}; - mg.aabb_min[0] = mi.local_aabb_min[0]; - mg.aabb_min[1] = mi.local_aabb_min[1]; - mg.aabb_min[2] = mi.local_aabb_min[2]; - mg.aabb_max[0] = mi.local_aabb_max[0]; - mg.aabb_max[1] = mi.local_aabb_max[1]; - mg.aabb_max[2] = mi.local_aabb_max[2]; - mesh_gpu.push_back(mg); - } - const size_t mesh_storage_bytes = mesh_gpu.size() * sizeof(MeshGpu); - m.mesh_storage = createBufferWithData( - device_, queue_, - mesh_gpu.data(), mesh_storage_bytes, - WGPUBufferUsage_Storage, - "model.mesh_storage"); - m.vram_bytes_ssbo += mesh_storage_bytes; - - // InstanceGpu storage. Rebase object_ids globally (same as non-streaming). - const uint32_t object_id_base = next_object_id_; - uint32_t max_local_id = 0; - std::vector inst_gpu; - inst_gpu.reserve(metadata.meta.instances.size()); - for (auto& ic : metadata.meta.instances) { - if (ic.object_id > max_local_id) max_local_id = ic.object_id; - ic.object_id = object_id_base + ic.object_id; - InstanceGpu ig = {}; - std::memcpy(ig.transform, ic.transform, sizeof(ig.transform)); - ig.object_id = ic.object_id; - ig.color_override_rgba8 = ic.color_override_rgba8; - ig.mesh_id = ic.mesh_id; - inst_gpu.push_back(ig); - } - next_object_id_ = object_id_base + max_local_id + 1; - const size_t inst_storage_bytes = inst_gpu.size() * sizeof(InstanceGpu); - m.instance_storage = createBufferWithData( - device_, queue_, - inst_gpu.data(), inst_storage_bytes, - WGPUBufferUsage_Storage, - "model.instance_storage"); - m.vram_bytes_ssbo += inst_storage_bytes; - - // Hand off CPU mirrors. - m.meshes = std::move(metadata.meta.meshes); - m.instances = std::move(metadata.meta.instances); - - // Streaming defers per-mesh vertex data until the owning chunk is - // loaded, so mesh-local volumes + the Area-tool CPU shadow can't - // be precomputed here. Both fill in per-chunk inside - // applyStreamedChunk as the bytes arrive. - m.mesh_local_volumes.assign(m.meshes.size(), 0.0); - m.mesh_triangles_cache.assign(m.meshes.size(), ModelGpuData::MeshTriangles{}); - // Default: assume opaque. applyStreamedChunk flips entries to 1 as - // their bytes arrive and a vertex-alpha-byte < 255 is observed. - m.mesh_has_alpha.assign(m.meshes.size(), uint8_t(0)); - - // object_id → instance index lookup. Volume tool reads it on every - // selection mutation; per-pick latency stays O(K) instead of O(K*N). - m.object_id_to_instance.clear(); - m.object_id_to_instance.reserve(m.instances.size()); - for (uint32_t i = 0; i < uint32_t(m.instances.size()); ++i) { - m.object_id_to_instance.emplace(m.instances[i].object_id, i); - } - - // Compute per-chunk world AABBs + instance-id lists from the - // instance_to_chunk mapping. Under spatial bucketing this captures - // each bucket's actual instance extent; under mesh-keyed it's - // equivalent to the old mesh_chunk_idx lookup since one mesh → one - // chunk → instances all land identically. - for (size_t ci = 0; ci < m.chunks.size(); ++ci) { - m.chunks[ci].instance_ids.reserve(m.instances.size() / m.chunks.size() + 4); - } - for (uint32_t inst_idx = 0; inst_idx < uint32_t(m.instances.size()); ++inst_idx) { - const auto& inst = m.instances[inst_idx]; - const uint32_t ci = instance_to_chunk[inst_idx]; - if (ci >= m.chunks.size()) continue; - auto& c = m.chunks[ci]; - for (int a = 0; a < 3; ++a) { - c.aabb_min[a] = std::min(c.aabb_min[a], inst.world_aabb_min[a]); - c.aabb_max[a] = std::max(c.aabb_max[a], inst.world_aabb_max[a]); - } - c.instance_ids.push_back(inst_idx); - } - - // Populate per-instance arrays from the per-chunk per-mesh offsets - // computed during chunk construction. Works for both planners: - // - mesh-keyed: each mesh in one chunk, offsets match the old - // per-mesh-array translation exactly (pixel-identical) - // - spatial: the same mesh_id may appear in different chunks at - // different offsets; the per-chunk table holds each chunk's own - // local offsets, so instance_*[i] reflects the chunk that - // instance i's bucket landed in - { - const size_t n_inst = m.instances.size(); - m.instance_chunk_idx.assign(n_inst, 0); - m.instance_base_vertex.assign(n_inst, 0); - m.instance_ebo_first_u32.assign(n_inst, 0); - m.instance_lod1_first_u32.assign(n_inst, 0); - for (size_t i = 0; i < n_inst; ++i) { - const uint32_t ci = instance_to_chunk[i]; - const uint32_t mi = m.instances[i].mesh_id; - if (ci >= chunk_mesh_offsets.size()) continue; - auto it = chunk_mesh_offsets[ci].find(mi); - if (it == chunk_mesh_offsets[ci].end()) continue; - m.instance_chunk_idx[i] = ci; - m.instance_base_vertex[i] = it->second.base_vertex; - m.instance_ebo_first_u32[i] = it->second.ebo_first; - m.instance_lod1_first_u32[i] = it->second.lod1_first; - } - } - - auto [inserted, _] = models_gpu_.emplace(model_id, std::move(m)); - ModelGpuData& mref = inserted->second; - - // Bind groups can't be built yet — they need vertex_storage from each - // chunk's load. The per-frame loader (commit 4) will buildModelBindGroup - // after a chunk becomes resident. - - Log::info().noquote().nospace() - << "[wgpu stream] applyCachedModel mid=" << model_id - << " verts=" << mref.vertex_bytes << "B (deferred)" - << " idx=" << mref.index_count - << " meshes=" << mref.mesh_count - << " instances=" << mref.instance_count - << " chunks=" << mref.chunks.size(); - - if (!initial_view_applied_) { - viewAll(); - initial_view_applied_ = true; - } - ensureSelectionFlagsBuffer(); - if (isExposed()) requestUpdate(); +void ViewportWindow::applyCachedModel(uint32_t model_id, StreamingSidecar metadata) { + core_.applyCachedModel(model_id, std::move(metadata)); } // ----------------------------------------------------------------------------- @@ -829,205 +496,13 @@ void ViewportWindow::applyCachedModel(uint32_t model_id, // same chunk planner the sidecar load uses. // ----------------------------------------------------------------------------- -static SidecarData& getOrCreateDirectStaging( - std::unordered_map>& staging, - uint32_t model_id) { - auto it = staging.find(model_id); - if (it == staging.end()) { - auto [it_new, _] = staging.emplace( - model_id, std::make_unique()); - return *it_new->second; - } - return *it->second; -} +// getOrCreateDirectStaging moved to ViewportCore (anon namespace) (#84-q). -void ViewportWindow::uploadMeshChunk(const MeshChunk& chunk) { - if (chunk.vertices.empty() || chunk.indices.empty()) return; - SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, chunk.model_id); +void ViewportWindow::uploadMeshChunk(const MeshChunk& chunk) { core_.uploadMeshChunk(chunk); } - // Streamer format: 7 floats / vertex (pos3 + normal3 + color-as-float). - // Same quantisation as SidecarBuilder::onMeshReady so direct-load and - // sidecar-load produce byte-identical GPU buffers. - const size_t n_verts = chunk.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS; +void ViewportWindow::uploadInstanceChunk(const InstanceChunk& chunk) { core_.uploadInstanceChunk(chunk); } - float bmin[3] = { std::numeric_limits::infinity(), - std::numeric_limits::infinity(), - std::numeric_limits::infinity() }; - float bmax[3] = { -std::numeric_limits::infinity(), - -std::numeric_limits::infinity(), - -std::numeric_limits::infinity() }; - for (size_t i = 0; i < n_verts; ++i) { - const float* v = chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS; - for (int a = 0; a < 3; ++a) { - if (v[a] < bmin[a]) bmin[a] = v[a]; - if (v[a] > bmax[a]) bmax[a] = v[a]; - } - } - float extent_recip[3]; - for (int a = 0; a < 3; ++a) { - const float ext = bmax[a] - bmin[a]; - extent_recip[a] = ext > 0.0f ? 1.0f / ext : 0.0f; - } - - const size_t vb_offset = s.vertices.size(); - s.vertices.resize(vb_offset + n_verts * INSTANCED_VERTEX_STRIDE_BYTES); - for (size_t i = 0; i < n_verts; ++i) { - quantizeVertex(chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS, - bmin, extent_recip, - s.vertices.data() + vb_offset - + i * INSTANCED_VERTEX_STRIDE_BYTES); - } - - const size_t ib_offset = s.indices.size(); - s.indices.insert(s.indices.end(), - chunk.indices.begin(), chunk.indices.end()); - - MeshInfo info{}; - info.vbo_byte_offset = uint32_t(vb_offset); - info.vertex_count = uint32_t(n_verts); - info.ebo_byte_offset = uint32_t(ib_offset * sizeof(uint32_t)); - info.index_count = uint32_t(chunk.indices.size()); - for (int a = 0; a < 3; ++a) { - info.local_aabb_min[a] = bmin[a]; - info.local_aabb_max[a] = bmax[a]; - } - info.first_instance = 0; - info.instance_count = 0; - info.lod1_ebo_byte_offset = 0; - info.lod1_index_count = 0; - - if (s.meshes.size() <= chunk.local_mesh_id) { - s.meshes.resize(chunk.local_mesh_id + 1); - } - s.meshes[chunk.local_mesh_id] = info; -} - -void ViewportWindow::uploadInstanceChunk(const InstanceChunk& chunk) { - SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, chunk.model_id); - - InstanceCpu inst{}; - inst.mesh_id = chunk.local_mesh_id; - inst.object_id = chunk.object_id; - inst.color_override_rgba8 = chunk.color_override_rgba8; - inst.model_id = chunk.model_id; - std::memcpy(inst.placement_transformation, chunk.transform, - sizeof(inst.placement_transformation)); - for (int i = 0; i < 16; ++i) { - inst.transform[i] = float(chunk.transform[i]); - } - std::memcpy(inst.world_aabb_min, chunk.world_aabb_min, sizeof(inst.world_aabb_min)); - std::memcpy(inst.world_aabb_max, chunk.world_aabb_max, sizeof(inst.world_aabb_max)); - - s.instances.push_back(inst); -} - -void ViewportWindow::finalizeModel(uint32_t model_id) { - auto it = pending_direct_loads_.find(model_id); - if (it == pending_direct_loads_.end()) { - Log::warn().nospace() - << "[wgpu direct] finalizeModel(" << model_id - << ") with no staged data; skipping"; - return; - } - // Move the staging out so the apply path can std::move from it without - // leaving a half-moved entry in the map mid-call. - std::unique_ptr staging_ptr = std::move(it->second); - pending_direct_loads_.erase(it); - SidecarData& s = *staging_ptr; - - if (!device_ || !queue_) { - Log::warn() << "[wgpu direct] finalizeModel without an initialised device"; - return; - } - if (s.meshes.empty() || s.instances.empty()) { - Log::info().nospace() << "[wgpu direct] finalizeModel(" << model_id - << "): empty staging (meshes=" << s.meshes.size() - << " instances=" << s.instances.size() << ")"; - return; - } - - // Build a StreamingSidecar around the staging so applyCachedModel can - // run its chunk planner over the same shape it expects from on-disk - // metadata. file_path is left empty — the streaming worker key off - // that to skip these chunks (they're already resident after the - // applyStreamedChunk loop below). - StreamingSidecar metadata; - metadata.meta = std::move(s); - metadata.vertex_section_offset = 0; - metadata.vertex_total_bytes = metadata.meta.vertices.size(); - metadata.index_section_offset = 0; - metadata.index_total_count = metadata.meta.indices.size(); - metadata.file_path.clear(); - - // applyCachedModel consumes meta.meshes / meta.instances (via std::move - // inside). The raw vertex / index bytes stay on `metadata.meta` until - // we gather them per-chunk below. - std::vector raw_vertices = std::move(metadata.meta.vertices); - std::vector raw_indices = std::move(metadata.meta.indices); - - applyCachedModel(model_id, std::move(metadata)); - - auto model_it = models_gpu_.find(model_id); - if (model_it == models_gpu_.end()) { - Log::warn().nospace() - << "[wgpu direct] finalizeModel(" << model_id - << "): applyCachedModel produced no model entry"; - return; - } - ModelGpuData& m = model_it->second; - - // Gather each chunk's vertex + index bytes from the staged buffers - // using the per-mesh chunk-local offsets the planner just produced. - // Same layout as makeChunkRequest's v_ranges/i_ranges, but the source - // is memory not a sidecar file. - size_t chunks_uploaded = 0; - for (size_t ci = 0; ci < m.chunks.size(); ++ci) { - auto& c = m.chunks[ci]; - if (c.mesh_ids.empty()) continue; - - std::vector vbytes(c.vertex_byte_size); - std::vector idx; - idx.reserve(c.index_count); - - for (uint32_t mi : c.mesh_ids) { - const MeshInfo& mesh = m.meshes[mi]; - const size_t vsz = size_t(mesh.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES; - if (vsz > 0) { - const size_t dst_off = size_t(m.mesh_chunk_local_base_vertex[mi]) - * INSTANCED_VERTEX_STRIDE_BYTES; - std::memcpy(vbytes.data() + dst_off, - raw_vertices.data() + mesh.vbo_byte_offset, vsz); - } - if (mesh.index_count > 0) { - const uint32_t* src = raw_indices.data() - + (mesh.ebo_byte_offset / sizeof(uint32_t)); - idx.insert(idx.end(), src, src + mesh.index_count); - } - } - // LOD1 indices: streamer doesn't emit them, but the planner reserves - // space for them in the chunk's index slice when m.meshes[mi] - // .lod1_index_count > 0. Direct-load never has LOD1, so this is a - // no-op walk; left here so the layout stays parallel to the - // sidecar gather. - - if (!core_.applyStreamedChunk(m, ci, vbytes, idx)) { - Log::warn().nospace() - << "[wgpu direct] finalizeModel(" << model_id - << "): applyStreamedChunk failed on chunk " << ci - << " (pool OOM?)"; - continue; - } - ++chunks_uploaded; - } - - Log::info().nospace() - << "[wgpu direct] finalizeModel mid=" << model_id - << " meshes=" << m.meshes.size() - << " instances=" << m.instances.size() - << " chunks=" << chunks_uploaded << "/" << m.chunks.size() - << " verts=" << raw_vertices.size() << "B" - << " idx=" << raw_indices.size(); -} +void ViewportWindow::finalizeModel(uint32_t model_id) { core_.finalizeModel(model_id); } // removeModel / resetScene / hideModel / showModel / // setFederatedFalseOrigin / setModelCoordinateOperation / diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index 1cfdb7a80d..2a579f4267 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -972,19 +972,10 @@ private: // Sidecar paths queued before init completes. std::deque pending_sidecars_; - // Direct-IFC staging buffers, keyed by streamer model_id. Populated - // by uploadMeshChunk / uploadInstanceChunk; consumed and cleared by - // finalizeModel. Shape matches SidecarData so the same chunk-planner - // + apply flow services both sidecar and direct-IFC loads. Held by - // unique_ptr so emplace / erase don't copy the (potentially huge) - // vertex byte vector when the map rehashes. - std::unordered_map> - pending_direct_loads_; - - // Set after the first model load triggers a viewAll(); prevents - // subsequent loads from snapping the camera away from where the - // user pointed it. - bool initial_view_applied_ = false; + // pending_direct_loads_ + initial_view_applied_ moved to ViewportCore + // (#84-q). initial_view_applied_ stays accessible here as a reference + // alias so VW::setCamera can flip it without poking through core_. + bool& initial_view_applied_; // Camera state at the previous render() for motion detection. Any // change means we apply the motion contribution threshold this frame