mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-24 20:50:02 +00:00
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:
@@ -65,6 +65,39 @@ test('renders the sample and orbits without WebGPU errors', async ({ page }) =>
|
|||||||
expect(gpuErrors, gpuErrors.join('\n')).toEqual([]);
|
expect(gpuErrors, gpuErrors.join('\n')).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('sample renders without any interaction (streaming settle loop)', async ({ page }) => {
|
||||||
|
// Regression for the blank-until-click stall: the main draw + cull precede
|
||||||
|
// driveStreamingLoads, so a freshly-resident chunk paints a frame later. On
|
||||||
|
// web's on-demand loop a single post-load requestFrame wasn't enough, so the
|
||||||
|
// sample stayed blank until some input re-armed the loop. The settle burst
|
||||||
|
// keeps frames coming until streaming converges. Here: NO mouse input at all
|
||||||
|
// — a centred patch (the framed cube) must differ from a corner patch
|
||||||
|
// (background). If the loop stalls blank, both patches are background.
|
||||||
|
const gpuErrors = [];
|
||||||
|
page.on('console', (msg) => {
|
||||||
|
if (/Uncaptured WebGPU error|is invalid|Not enough memory left/i.test(msg.text()))
|
||||||
|
gpuErrors.push(msg.text());
|
||||||
|
});
|
||||||
|
await page.goto('/IfcViewerWeb.html');
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => !!(window.Module && window.Module._app_ptr), null, { timeout: 30_000 });
|
||||||
|
|
||||||
|
// Settle window — strictly no pointer events.
|
||||||
|
await page.waitForTimeout(1500);
|
||||||
|
|
||||||
|
const box = await page.locator('#viewer-canvas').boundingBox();
|
||||||
|
const patch = (cx, cy) => page.screenshot({
|
||||||
|
clip: { x: Math.round(cx - 12), y: Math.round(cy - 12), width: 24, height: 24 },
|
||||||
|
});
|
||||||
|
const center = await patch(box.x + box.width / 2, box.y + box.height / 2);
|
||||||
|
const corner = await patch(box.x + 16, box.y + 16);
|
||||||
|
expect(
|
||||||
|
Buffer.compare(center, corner),
|
||||||
|
'centre patch matches corner — sample never rendered without interaction',
|
||||||
|
).not.toBe(0);
|
||||||
|
expect(gpuErrors, gpuErrors.join('\n')).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
test('loads a user-picked sidecar through the Blob.slice byte-range path', async ({ page }) => {
|
test('loads a user-picked sidecar through the Blob.slice byte-range path', async ({ page }) => {
|
||||||
// Exercises #88: the picked File is read via Blob.slice (metadata head/tail
|
// Exercises #88: the picked File is read via Blob.slice (metadata head/tail
|
||||||
// + per-chunk byte ranges) WITHOUT copying the whole file into the wasm
|
// + per-chunk byte ranges) WITHOUT copying the whole file into the wasm
|
||||||
|
|||||||
@@ -2305,7 +2305,35 @@ void ViewportCore::driveStreamingLoads() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
loads += enqueued;
|
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_loads_this_frame_ = loads;
|
||||||
streaming_more_pending_ = more_pending;
|
streaming_more_pending_ = more_pending;
|
||||||
|
|||||||
@@ -873,6 +873,14 @@ private:
|
|||||||
int streaming_loads_this_frame_ = 0;
|
int streaming_loads_this_frame_ = 0;
|
||||||
bool streaming_more_pending_ = false;
|
bool streaming_more_pending_ = false;
|
||||||
|
|
||||||
|
// Settle burst: keep the render loop alive for a few frames after any
|
||||||
|
// streaming activity so the cull→load→display latency (the draw + cull
|
||||||
|
// precede driveStreamingLoads, so a freshly-resident chunk paints a frame
|
||||||
|
// later) flushes even under an on-demand render loop (web). Bounded, so
|
||||||
|
// the loop still quiesces when streaming is done. See driveStreamingLoads.
|
||||||
|
static constexpr int kStreamingSettleFrames = 4;
|
||||||
|
int streaming_settle_frames_ = 0;
|
||||||
|
|
||||||
// Per-frame breakdown counters consumed by the WGPU_STREAM_DEBUG
|
// Per-frame breakdown counters consumed by the WGPU_STREAM_DEBUG
|
||||||
// log. All reset at the top of driveStreamingLoads.
|
// log. All reset at the top of driveStreamingLoads.
|
||||||
int streaming_candidates_this_frame_ = 0;
|
int streaming_candidates_this_frame_ = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user