ifcviewer: fix blank-until-interaction stall on web (streaming settle burst)

On first web load the sample stayed blank until a click/drag, then popped
in. Root cause: the main draw + cull run before driveStreamingLoads in
render(), so a chunk that becomes resident there is only painted a frame
later. On desktop the streaming thread keeps inFlightApprox() > 0 during a
load, so the render loop keeps ticking and the next frame paints it. On web
the sync MEMFS / Blob load finishes instantly (inFlightApprox stays 0), so
the single post-load requestFrame fired once and the on-demand loop went
idle before the geometry was ever drawn — until some input re-armed it.

Fix: arm a bounded settle burst (kStreamingSettleFrames) whenever there's
streaming activity — a load this frame, work still queued, or a visible
chunk not yet resident — and bleed it down over the next few frames, each
requesting one more. Covers the cull→display latency under an on-demand
loop and still quiesces at idle (no busy-rendering). General, not web-only.

Regression test: the sample must render with NO pointer input — a centred
patch (the framed cube) differs from a corner patch (background); a blank
stall leaves both as background. Verified empirically with a no-interaction
probe (canvas went from a static blank hash to a stable rendered one).
107/107 unit tests pass; 4/4 web smoke tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-06-30 10:11:58 +10:00
parent 2b58d7be74
commit 3cc759d72b
3 changed files with 70 additions and 1 deletions
+29 -1
View File
@@ -2305,7 +2305,35 @@ void ViewportCore::driveStreamingLoads() {
}
}
loads += enqueued;
if (loads > 0 || streaming_thread_.inFlightApprox() > 0) host_->requestFrame();
// Keep the render loop alive while streaming settles. The main draw + cull
// run *before* this point in render(), so a chunk that becomes resident
// here is only drawn on a later frame — and on web a sync load finishes
// instantly (inFlightApprox stays 0), so a single requestFrame after a
// load isn't enough to flush that cull→display latency. Arm a short settle
// burst whenever there's streaming activity (a load this frame, work still
// queued, or a visible chunk not yet resident) and bleed it down over the
// next few frames so an on-demand render loop doesn't stall before the
// geometry actually appears. Bounded, so the loop still quiesces at idle.
bool visible_pending = false;
for (const auto& [mid, m] : models_gpu_) {
if (m.streaming_file_path.empty() || m.hidden) continue;
for (const auto& c : m.chunks) {
if (!c.is_resident && (c.frustum_visible_count > 0 || c.is_loading)) {
visible_pending = true;
break;
}
}
if (visible_pending) break;
}
if (loads > 0 || more_pending || visible_pending
|| streaming_thread_.inFlightApprox() > 0) {
streaming_settle_frames_ = kStreamingSettleFrames;
}
if (streaming_settle_frames_ > 0) {
--streaming_settle_frames_;
host_->requestFrame();
}
streaming_loads_this_frame_ = loads;
streaming_more_pending_ = more_pending;