From 2c5e2d1685f4b54d84c6c175df33f0d0ee43951c Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 30 Jun 2026 13:50:37 +1000 Subject: [PATCH] ifcviewer-web: fetch a chunk's vertex + index ranges concurrently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit beginWebChunkLoad read the vertex ranges, then in the completion callback read the index ranges, then applied — two serial round trips per chunk. On a network that's the dominant per-chunk latency. Now both reads fire at once and a small shared join (payloads + per-read done/ok flags) runs the apply when the second lands, halving per-chunk RTT. Model re-lookup still happens at apply time, so a resetScene mid-flight is dropped safely. Per-chunk concurrency stacks with the existing across-chunk concurrency (driveStreamingLoads issues several loads per frame); the browser caps simultaneous connections per origin, so no explicit in-flight cap is needed. 6/6 web smoke + 107/107 unit pass. Co-Authored-By: Claude Opus 4.8 --- src/ifcviewer/ViewportCore.cpp | 69 ++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/src/ifcviewer/ViewportCore.cpp b/src/ifcviewer/ViewportCore.cpp index afda2302b0..c2930e24c3 100644 --- a/src/ifcviewer/ViewportCore.cpp +++ b/src/ifcviewer/ViewportCore.cpp @@ -3326,36 +3326,47 @@ void ViewportCore::beginWebChunkLoad(std::uint32_t model_id, std::size_t chunk_i for (const auto& [first_u32, count] : req.i_ranges) i_byte_ranges.emplace_back(first_u32 * 4u, count * 4u); - // Read vertex ranges, then index ranges, then apply. Re-look-up the model - // in each callback: a resetScene() could have landed mid-flight, in which - // case the model id is gone and we simply drop the result. + // Fire the vertex and index range reads CONCURRENTLY and join when both + // land — over a network this halves per-chunk latency vs reading vertices + // then indices serially (two round trips → one). The join holds both + // payloads + completion flags; whichever read finishes second runs the + // apply. Re-look-up the model at apply time: a resetScene() could have + // landed mid-flight, in which case the model id is gone and we drop it. + struct ChunkJoin { + std::vector vbytes; + std::vector idx; + bool v_done = false, i_done = false, v_ok = false, i_ok = false; + }; + auto join = std::make_shared(); + std::function finish = [this, model_id, chunk_idx, join]() { + if (!join->v_done || !join->i_done) return; // wait for the other read + auto mit = models_gpu_.find(model_id); + if (mit == models_gpu_.end()) return; + ModelGpuData& mm = mit->second; + if (chunk_idx >= mm.chunks.size()) return; + if (!join->v_ok || !join->i_ok) { mm.chunks[chunk_idx].is_loading = false; return; } + if (!applyStreamedChunk(mm, chunk_idx, join->vbytes, join->idx)) + mm.chunks[chunk_idx].is_loading = false; // pool full; retry later + else + host_->requestFrame(); + }; + webReadRangesAsync(vsec, v_ranges, - [this, model_id, chunk_idx, isec, i_byte_ranges] - (bool ok, std::vector&& vbytes) { - auto mit = models_gpu_.find(model_id); - if (mit == models_gpu_.end()) return; - if (chunk_idx >= mit->second.chunks.size()) return; - if (!ok) { mit->second.chunks[chunk_idx].is_loading = false; return; } - - auto vb = std::make_shared>(std::move(vbytes)); - webReadRangesAsync(isec, i_byte_ranges, - [this, model_id, chunk_idx, vb] - (bool ok2, std::vector&& ibytes) { - auto mit2 = models_gpu_.find(model_id); - if (mit2 == models_gpu_.end()) return; - ModelGpuData& mm = mit2->second; - if (chunk_idx >= mm.chunks.size()) return; - if (!ok2) { mm.chunks[chunk_idx].is_loading = false; return; } - - std::vector idx(ibytes.size() / sizeof(std::uint32_t)); - if (!idx.empty()) - std::memcpy(idx.data(), ibytes.data(), - idx.size() * sizeof(std::uint32_t)); - if (!applyStreamedChunk(mm, chunk_idx, *vb, idx)) - mm.chunks[chunk_idx].is_loading = false; // pool full; retry later - else - host_->requestFrame(); - }); + [join, finish](bool ok, std::vector&& vbytes) { + join->v_ok = ok; + join->vbytes = std::move(vbytes); + join->v_done = true; + finish(); + }); + webReadRangesAsync(isec, i_byte_ranges, + [join, finish](bool ok, std::vector&& ibytes) { + join->i_ok = ok; + join->idx.resize(ibytes.size() / sizeof(std::uint32_t)); + if (!join->idx.empty()) + std::memcpy(join->idx.data(), ibytes.data(), + join->idx.size() * sizeof(std::uint32_t)); + join->i_done = true; + finish(); }); }