mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 15:53:00 +00:00
ifcviewer-web: expose frame stats and per-model unload/load to the page
The memory work (cache budget, pressure handling, unloadModel) lives in
ViewportCore and so already ran in the wasm, but the page could not see
or use any of it: the web host had no onFrameStats, and there were no
bindings for residency.
- WebViewportHost latches the last FrameStats; ifcv_get_frame_stats_c
hands them to JS as doubles, and viewer.stats() returns {fps,
frameTimeMs, objects, triangles, drawCalls, vram{used, capacity,
budget}, workingSet{chunks, chunksMissing, missingBytes}} — the same
figures BonsaiViewer's status bar shows. Device-wide VRAM is omitted:
there is no query for it on web.
- viewer.unloadModel / loadModel / modelUnloaded / modelVramBytes, keyed
by source id like the other per-model calls.
- The demo page shows a GPU memory line that turns into a "full: N of M
visible chunks not loaded" notice once a shortfall persists for 3 s,
and each model's MB with an Unload/Load button.
- memory.spec.mjs covers stats() and the unload/load round trip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -115,7 +115,7 @@ target_link_options(IfcViewerWeb PRIVATE
|
||||
# EMSCRIPTEN_KEEPALIVE alone keeps the symbols in the binary but doesn't
|
||||
# add them to Module. ccall lets the host page (web/ifcviewer.js) pass a JS string (the ?model
|
||||
# URL) to load_sidecar_from_url_c without manual heap marshalling.
|
||||
"-sEXPORTED_FUNCTIONS=['_main','_malloc','_free','_raf_tick_c','_load_sidecar_from_source_c','_clear_scene_c','_ifcv_on_range_done','_ifcv_chunks_resident_c','_ifcv_chunks_total_c','_ifcv_model_count_c','_ifcv_model_resident_c','_ifcv_model_total_c','_ifcv_bytes_total_c','_ifcv_bytes_needed_c','_ifcv_bytes_loaded_c','_view_all_c','_frame_selection_c','_toggle_projection_c','_projection_is_ortho_c','_standard_view_c','_toggle_fly_c','_fly_is_active_c','_hide_selected_c','_isolate_selected_c','_show_all_c','_hide_all_c','_toggle_xray_c','_xray_is_active_c','_toggle_section_c','_clear_section_c','_section_is_active_c','_ifcv_get_camera_c','_ifcv_set_camera_c','_ifcv_set_ortho_c','_ifcv_set_nav_preset_c','_ifcv_set_background_c','_ifcv_get_selection_c','_ifcv_get_active_object_c','_ifcv_apply_selection_c','_ifcv_set_visible_c','_ifcv_get_hidden_c','_ifcv_set_color_c','_ifcv_clear_colors_c','_ifcv_request_objects_c','_ifcv_set_selection_outline_c','_ifcv_selection_outline_is_on_c','_ifcv_set_federation_unit_c','_ifcv_set_false_origin_c','_ifcv_get_false_origin_c','_ifcv_set_model_transform_c','_ifcv_clear_model_transform_c','_ifcv_set_model_name_c','_ifcv_get_model_georef_c']"
|
||||
"-sEXPORTED_FUNCTIONS=['_main','_malloc','_free','_raf_tick_c','_load_sidecar_from_source_c','_clear_scene_c','_ifcv_on_range_done','_ifcv_chunks_resident_c','_ifcv_chunks_total_c','_ifcv_model_count_c','_ifcv_model_resident_c','_ifcv_model_total_c','_ifcv_bytes_total_c','_ifcv_bytes_needed_c','_ifcv_bytes_loaded_c','_view_all_c','_frame_selection_c','_toggle_projection_c','_projection_is_ortho_c','_standard_view_c','_toggle_fly_c','_fly_is_active_c','_hide_selected_c','_isolate_selected_c','_show_all_c','_hide_all_c','_toggle_xray_c','_xray_is_active_c','_toggle_section_c','_clear_section_c','_section_is_active_c','_ifcv_get_camera_c','_ifcv_set_camera_c','_ifcv_set_ortho_c','_ifcv_set_nav_preset_c','_ifcv_set_background_c','_ifcv_get_selection_c','_ifcv_get_active_object_c','_ifcv_apply_selection_c','_ifcv_set_visible_c','_ifcv_get_hidden_c','_ifcv_set_color_c','_ifcv_clear_colors_c','_ifcv_request_objects_c','_ifcv_set_selection_outline_c','_ifcv_selection_outline_is_on_c','_ifcv_set_federation_unit_c','_ifcv_set_false_origin_c','_ifcv_get_false_origin_c','_ifcv_set_model_transform_c','_ifcv_clear_model_transform_c','_ifcv_set_model_name_c','_ifcv_get_model_georef_c','_ifcv_get_frame_stats_c','_ifcv_unload_model_c','_ifcv_load_model_c','_ifcv_model_unloaded_c','_ifcv_model_vram_bytes_c']"
|
||||
# ccall: the host page (web/ifcviewer.js) passes the ?model URL string to load_sidecar_from_url_c,
|
||||
# and the nav-preset name to ifcv_set_nav_preset_c.
|
||||
# HEAPU8: lets tooling/tests read the wasm heap size (e.g. to verify a large
|
||||
|
||||
@@ -54,9 +54,16 @@ public:
|
||||
// core.render().
|
||||
bool consumeFrameRequest();
|
||||
|
||||
// The most recent per-frame stats (fps, VRAM, working set). Latched
|
||||
// here so the page can read them whenever it likes (ifcv_get_frame_stats_c)
|
||||
// instead of being called back every frame across the wasm boundary.
|
||||
void onFrameStats(const FrameStats& stats) override { last_stats_ = stats; }
|
||||
const FrameStats& lastFrameStats() const { return last_stats_; }
|
||||
|
||||
private:
|
||||
std::string canvas_selector_;
|
||||
bool request_frame_pending_ = true; // arm an initial frame
|
||||
FrameStats last_stats_ = {};
|
||||
};
|
||||
|
||||
#endif // WEBVIEWPORTHOST_H
|
||||
|
||||
@@ -926,6 +926,62 @@ extern "C" EMSCRIPTEN_KEEPALIVE double ifcv_bytes_loaded_c() {
|
||||
return double(loaded_bytes);
|
||||
}
|
||||
|
||||
// ---- Frame stats + GPU residency ------------------------------------------
|
||||
|
||||
// The latest FrameStats as doubles, in this order (see FrameStats.h):
|
||||
// 0 fps, 1 frame_time_ms, 2 total_objects, 3 visible_objects,
|
||||
// 4 total_triangles, 5 visible_triangles, 6 draw_calls,
|
||||
// 7 vram_used_bytes, 8 vram_capacity_bytes, 9 vram_budget_bytes,
|
||||
// 10 chunks_wanted, 11 chunks_wanted_missing, 12 wanted_missing_bytes.
|
||||
// Device-wide VRAM is not included: there is no query for it on web.
|
||||
// Returns the number of values written (0 before the first frame).
|
||||
constexpr int kFrameStatsValues = 13;
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE int ifcv_get_frame_stats_c(double* out, int capacity) {
|
||||
if (!g_app || !out || capacity < kFrameStatsValues) return 0;
|
||||
const FrameStats& s = g_app->host.lastFrameStats();
|
||||
out[0] = s.fps;
|
||||
out[1] = s.frame_time_ms;
|
||||
out[2] = s.total_objects;
|
||||
out[3] = s.visible_objects;
|
||||
out[4] = s.total_triangles;
|
||||
out[5] = s.visible_triangles;
|
||||
out[6] = s.gl_draw_calls;
|
||||
out[7] = double(s.vram_used_bytes);
|
||||
out[8] = double(s.vram_capacity_bytes);
|
||||
out[9] = double(s.vram_budget_bytes);
|
||||
out[10] = s.chunks_wanted;
|
||||
out[11] = s.chunks_wanted_missing;
|
||||
out[12] = double(s.wanted_missing_bytes);
|
||||
return kFrameStatsValues;
|
||||
}
|
||||
|
||||
// Per-model GPU residency, keyed by source id like the other per-model
|
||||
// exports. Unload frees everything the model holds on the GPU while it stays
|
||||
// in the scene; load brings it back (0 if the device cannot fit its buffers).
|
||||
// Neither touches visibility.
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void ifcv_unload_model_c(int source_id) {
|
||||
if (!g_app) return;
|
||||
const std::uint32_t session_model_id = g_app->federation.sessionModelId(source_id);
|
||||
if (session_model_id == 0) return;
|
||||
g_app->core.unloadModel(session_model_id);
|
||||
}
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE int ifcv_load_model_c(int source_id) {
|
||||
if (!g_app) return 0;
|
||||
const std::uint32_t session_model_id = g_app->federation.sessionModelId(source_id);
|
||||
if (session_model_id == 0) return 0;
|
||||
return g_app->core.loadModel(session_model_id) ? 1 : 0;
|
||||
}
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE int ifcv_model_unloaded_c(int source_id) {
|
||||
if (!g_app) return 0;
|
||||
const std::uint32_t session_model_id = g_app->federation.sessionModelId(source_id);
|
||||
return session_model_id != 0 && g_app->core.isModelUnloaded(session_model_id) ? 1 : 0;
|
||||
}
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE double ifcv_model_vram_bytes_c(int source_id) {
|
||||
if (!g_app) return 0.0;
|
||||
const std::uint32_t session_model_id = g_app->federation.sessionModelId(source_id);
|
||||
return session_model_id == 0 ? 0.0 : double(g_app->core.modelVramBytes(session_model_id));
|
||||
}
|
||||
|
||||
int main(int /*argc*/, char** /*argv*/) {
|
||||
Log::info() << "ifcviewer-web: starting";
|
||||
g_app = new AppState();
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
// GPU memory as the host page sees it: the per-frame stats (cache occupancy,
|
||||
// working set) and the per-model unload/load lever. Mirrors what
|
||||
// BonsaiViewer's status bar and Models panel show on desktop.
|
||||
|
||||
async function open(page) {
|
||||
const errors = [];
|
||||
page.on('console', (msg) => {
|
||||
const t = msg.text();
|
||||
if (/Uncaptured WebGPU error|is invalid|Not enough memory left/i.test(t)) errors.push(t);
|
||||
});
|
||||
page.on('pageerror', (e) => errors.push('pageerror: ' + e.message));
|
||||
await page.goto('/scripting.html');
|
||||
await page.waitForFunction(() => !!(window.viewer && window.viewer.isLive()), null,
|
||||
{ timeout: 30_000 });
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Add the sample as a real source (the embedded one has no source id) and
|
||||
// wait until every chunk is resident.
|
||||
async function addSampleAndSettle(page) {
|
||||
const sid = await page.evaluate(async () => {
|
||||
const v = window.viewer;
|
||||
const loaded = new Promise((resolve) => {
|
||||
const off = v.onModelLoaded((d) => { off(); resolve(d); });
|
||||
});
|
||||
const sid = await v.addUrl('/sample.ifcview', { replace: true, name: 'mem' });
|
||||
await loaded;
|
||||
return sid;
|
||||
});
|
||||
await settled(page);
|
||||
return sid;
|
||||
}
|
||||
|
||||
async function settled(page) {
|
||||
await page.waitForFunction(() => {
|
||||
const v = window.viewer;
|
||||
if (!v.modelCount()) return false;
|
||||
for (let i = 0; i < v.modelCount(); ++i) {
|
||||
const p = v.modelProgress(i);
|
||||
if (!(p.total > 0 && p.resident === p.total)) return false;
|
||||
}
|
||||
return true;
|
||||
}, null, { timeout: 30_000 });
|
||||
}
|
||||
|
||||
test('stats() reports the frame, the cache and the working set', async ({ page }) => {
|
||||
const errors = await open(page);
|
||||
await addSampleAndSettle(page);
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
const s = await page.evaluate(() => window.viewer.stats());
|
||||
expect(s).not.toBeNull();
|
||||
expect(s.fps).toBeGreaterThan(0);
|
||||
expect(s.frameTimeMs).toBeGreaterThan(0);
|
||||
expect(s.objects.total).toBeGreaterThan(0);
|
||||
expect(s.triangles.total).toBeGreaterThan(0);
|
||||
|
||||
// Resident geometry occupies the cache, within its capacity, and on web
|
||||
// the cache is bounded from the start (the wasm heap cap).
|
||||
expect(s.vram.usedBytes).toBeGreaterThan(0);
|
||||
expect(s.vram.usedBytes).toBeLessThanOrEqual(s.vram.capacityBytes);
|
||||
expect(s.vram.budgetBytes).toBeGreaterThan(0);
|
||||
|
||||
// Everything the camera wants is resident once settled.
|
||||
expect(s.workingSet.chunks).toBeGreaterThan(0);
|
||||
expect(s.workingSet.chunksMissing).toBe(0);
|
||||
expect(s.workingSet.missingBytes).toBe(0);
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
test('unloadModel frees the model\'s GPU memory and loadModel streams it back', async ({ page }) => {
|
||||
const errors = await open(page);
|
||||
const sid = await addSampleAndSettle(page);
|
||||
|
||||
const before = await page.evaluate((sid) => ({
|
||||
unloaded: window.viewer.modelUnloaded(sid),
|
||||
bytes: window.viewer.modelVramBytes(sid),
|
||||
used: window.viewer.stats().vram.usedBytes,
|
||||
}), sid);
|
||||
expect(before.unloaded).toBe(false);
|
||||
expect(before.bytes).toBeGreaterThan(0);
|
||||
|
||||
// Unload: the model's bytes go to zero immediately, and it stays listed.
|
||||
const after = await page.evaluate((sid) => {
|
||||
const v = window.viewer;
|
||||
v.unloadModel(sid);
|
||||
return {
|
||||
unloaded: v.modelUnloaded(sid),
|
||||
bytes: v.modelVramBytes(sid),
|
||||
modelCount: v.modelCount(),
|
||||
};
|
||||
}, sid);
|
||||
expect(after.unloaded).toBe(true);
|
||||
expect(after.bytes).toBe(0);
|
||||
expect(after.modelCount).toBe(1);
|
||||
|
||||
// The cache reflects the release on the next frame.
|
||||
await page.waitForFunction((used) => {
|
||||
const s = window.viewer.stats();
|
||||
return s && s.vram.usedBytes < used;
|
||||
}, before.used, { timeout: 10_000 });
|
||||
|
||||
// Load: the buffers come back and the chunks stream in again.
|
||||
const reloaded = await page.evaluate((sid) => window.viewer.loadModel(sid), sid);
|
||||
expect(reloaded).toBe(true);
|
||||
expect(await page.evaluate((sid) => window.viewer.modelUnloaded(sid), sid)).toBe(false);
|
||||
await settled(page);
|
||||
const restored = await page.evaluate((sid) => window.viewer.modelVramBytes(sid), sid);
|
||||
expect(restored).toBe(before.bytes);
|
||||
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
@@ -42,6 +42,10 @@
|
||||
ul#model-list .name b { font-weight: 600; overflow: hidden; text-overflow: ellipsis;
|
||||
white-space: nowrap; }
|
||||
ul#model-list .pct { color: #8a93a6; flex: 0 0 auto; }
|
||||
ul#model-list .mem { color: #8a93a6; font-size: 11px; margin-left: 8px; flex: 0 0 auto; }
|
||||
ul#model-list .mem button { font-size: 11px; padding: 1px 6px; margin-left: 6px; }
|
||||
ul#model-list li.unloaded b { font-style: italic; color: #6f7988; }
|
||||
#gpu-memory.full { color: #e0a040; }
|
||||
.bar { height: 4px; margin-top: 5px; border-radius: 2px; background: #232833; overflow: hidden; }
|
||||
.bar > i { display: block; height: 100%; width: 0%; background: #3182ce; }
|
||||
.empty { color: #6f7988; font-size: 12px; padding: 4px 0; }
|
||||
@@ -92,6 +96,7 @@
|
||||
<div class="card">
|
||||
<h2>Models in scene</h2>
|
||||
<ul id="model-list"><li class="empty">No models loaded.</li></ul>
|
||||
<div class="hint" id="gpu-memory">GPU memory: —</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -124,16 +129,61 @@
|
||||
listEl.innerHTML = '';
|
||||
models.forEach(function (m, i) {
|
||||
var li = document.createElement('li');
|
||||
if (m.unloaded) li.className = 'unloaded';
|
||||
var pct = m.total > 0 ? Math.round(100 * m.resident / m.total) : 0;
|
||||
var label = m.total > 0 ? pct + '%' : '…';
|
||||
var label = m.unloaded ? 'unloaded' : m.total > 0 ? pct + '%' : '…';
|
||||
var mem = m.unloaded ? '' : Math.round(m.vram / (1024 * 1024)) + ' MB';
|
||||
li.innerHTML =
|
||||
'<div class="name"><b title="' + m.name + '">' + m.name + '</b>' +
|
||||
'<span class="mem">' + mem + '<button data-i="' + i + '">' +
|
||||
(m.unloaded ? 'Load' : 'Unload') + '</button></span>' +
|
||||
'<span class="pct">' + label + '</span></div>' +
|
||||
'<div class="bar"><i style="width:' + pct + '%"></i></div>';
|
||||
'<div class="bar"><i style="width:' + (m.unloaded ? 0 : pct) + '%"></i></div>';
|
||||
listEl.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
// Unload frees a model's GPU memory while it stays in the scene — the lever
|
||||
// when the GPU memory line reports chunks that cannot be loaded.
|
||||
listEl.addEventListener('click', function (ev) {
|
||||
var btn = ev.target.closest('button[data-i]');
|
||||
if (!btn || !activeViewer) return;
|
||||
var m = models[+btn.dataset.i];
|
||||
if (!m || m.sid === undefined) return;
|
||||
if (m.unloaded) {
|
||||
if (!activeViewer.loadModel(m.sid)) { hintEl.textContent = 'Not enough GPU memory to load ' + m.name; return; }
|
||||
} else {
|
||||
activeViewer.unloadModel(m.sid);
|
||||
}
|
||||
m.unloaded = activeViewer.modelUnloaded(m.sid);
|
||||
renderList();
|
||||
});
|
||||
|
||||
var gpuMemEl = document.getElementById('gpu-memory');
|
||||
var shortfallSince = 0;
|
||||
var activeViewer = null; // set once IfcViewer.create resolves
|
||||
function renderGpuMemory(viewer) {
|
||||
var s = viewer.stats();
|
||||
if (!s) return;
|
||||
var mb = function (b) { return Math.round(b / (1024 * 1024)); };
|
||||
var text = 'GPU memory: ' + mb(s.vram.usedBytes) + ' / ' + mb(s.vram.capacityBytes) + ' MB';
|
||||
if (s.vram.budgetBytes && s.vram.budgetBytes !== s.vram.capacityBytes) {
|
||||
text += ' (budget ' + mb(s.vram.budgetBytes) + ')';
|
||||
}
|
||||
// A few missing chunks right after a camera move are normal; a shortfall
|
||||
// that persists means the view does not fit — say so.
|
||||
var now = performance.now();
|
||||
if (!s.workingSet.chunksMissing) shortfallSince = 0;
|
||||
else if (!shortfallSince) shortfallSince = now;
|
||||
var full = shortfallSince && now - shortfallSince > 3000;
|
||||
if (full) {
|
||||
text += ' — full: ' + s.workingSet.chunksMissing + ' of ' + s.workingSet.chunks +
|
||||
' visible chunks (' + mb(s.workingSet.missingBytes) + ' MB) not loaded. Unload a model to make room.';
|
||||
}
|
||||
if (gpuMemEl.textContent !== text) gpuMemEl.textContent = text;
|
||||
gpuMemEl.classList.toggle('full', !!full);
|
||||
}
|
||||
|
||||
function setSelection(guid, modelName) {
|
||||
selModelEl.textContent = modelName || '—';
|
||||
if (guid) { selGuidEl.textContent = guid; selGuidEl.classList.remove('none'); }
|
||||
@@ -154,16 +204,20 @@
|
||||
// Keep clearing until it's gone; stop once the user adds their own model.
|
||||
if (!userAddedAny && viewer.modelCount() > 0) viewer.clearScene();
|
||||
if (!models.length) return;
|
||||
renderGpuMemory(viewer);
|
||||
var changed = false;
|
||||
for (var i = 0; i < models.length; i++) {
|
||||
var p = viewer.modelProgress(i);
|
||||
if (p.resident !== models[i].resident || p.total !== models[i].total) {
|
||||
models[i].resident = p.resident; models[i].total = p.total; changed = true;
|
||||
var vram = models[i].sid !== undefined ? viewer.modelVramBytes(models[i].sid) : 0;
|
||||
if (p.resident !== models[i].resident || p.total !== models[i].total
|
||||
|| Math.round(vram / (1024 * 1024)) !== Math.round(models[i].vram / (1024 * 1024))) {
|
||||
models[i].resident = p.resident; models[i].total = p.total; models[i].vram = vram; changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) renderList();
|
||||
},
|
||||
}).then(function (viewer) {
|
||||
activeViewer = viewer;
|
||||
// Report the picked object's model + IFC GlobalId in our own DOM (empty on
|
||||
// deselect). sel.modelIndex indexes our JS model list (load order).
|
||||
viewer.onSelect(function (sel) {
|
||||
@@ -177,7 +231,10 @@
|
||||
var urlInput = document.getElementById('url-input');
|
||||
var urlBtn = document.getElementById('url-btn');
|
||||
|
||||
function addModelEntry(name) { models.push({ name: name, resident: 0, total: 0 }); renderList(); }
|
||||
function addModelEntry(name, sid) {
|
||||
models.push({ name: name, sid: sid, resident: 0, total: 0, vram: 0, unloaded: false });
|
||||
renderList();
|
||||
}
|
||||
|
||||
viewer.ready.then(function () {
|
||||
hintEl.textContent = 'Ready — add a .ifcview model.';
|
||||
@@ -188,7 +245,7 @@
|
||||
fileInput.addEventListener('change', function (ev) {
|
||||
if (ev.target.files.length) userAddedAny = true;
|
||||
Array.prototype.forEach.call(ev.target.files, function (file) {
|
||||
viewer.addFile(file).then(function () { addModelEntry(file.name); });
|
||||
viewer.addFile(file).then(function (sid) { addModelEntry(file.name, sid); });
|
||||
});
|
||||
fileInput.value = '';
|
||||
});
|
||||
@@ -198,8 +255,8 @@
|
||||
if (!url) return;
|
||||
userAddedAny = true;
|
||||
urlBtn.disabled = true;
|
||||
viewer.addUrl(url).then(function () {
|
||||
addModelEntry(url.split('/').pop() || url);
|
||||
viewer.addUrl(url).then(function (sid) {
|
||||
addModelEntry(url.split('/').pop() || url, sid);
|
||||
urlInput.value = '';
|
||||
}).catch(function (e) {
|
||||
hintEl.textContent = 'URL load failed: ' + e.message;
|
||||
|
||||
@@ -530,6 +530,45 @@
|
||||
};
|
||||
},
|
||||
|
||||
// The latest frame's statistics — what BonsaiViewer's status bar shows.
|
||||
// `vram` is the streamed-geometry cache: bytes held by resident chunks,
|
||||
// the pool's current capacity, and the budget it may grow to (0 while
|
||||
// unbounded). `workingSet` is what the camera wants resident: chunks in
|
||||
// view and large enough to draw, how many of those are not resident,
|
||||
// and their size — transiently non-zero after a camera move, and
|
||||
// persistently non-zero when the scene does not fit in GPU memory
|
||||
// (unload a model to make room). Null before the first frame.
|
||||
stats: function () {
|
||||
const n = 13;
|
||||
const ptr = Module._malloc(n * 8);
|
||||
try {
|
||||
if (!Module._ifcv_get_frame_stats_c(ptr, n)) return null;
|
||||
const d = Module.HEAPF64.subarray(ptr >>> 3, (ptr >>> 3) + n);
|
||||
return {
|
||||
fps: d[0],
|
||||
frameTimeMs: d[1],
|
||||
objects: { visible: d[3], total: d[2] },
|
||||
triangles: { visible: d[5], total: d[4] },
|
||||
drawCalls: d[6],
|
||||
vram: { usedBytes: d[7], capacityBytes: d[8], budgetBytes: d[9] },
|
||||
workingSet: { chunks: d[10], chunksMissing: d[11], missingBytes: d[12] },
|
||||
};
|
||||
} finally {
|
||||
Module._free(ptr);
|
||||
}
|
||||
},
|
||||
|
||||
// GPU residency per model, by source id. Unloading frees everything
|
||||
// the model holds on the GPU while it stays in the scene (its
|
||||
// visibility untouched); loading brings it back, streaming the
|
||||
// geometry in again on demand. loadModel resolves false when the
|
||||
// device cannot fit the model's buffers. This is the lever when
|
||||
// stats().workingSet.chunksMissing stays above zero.
|
||||
unloadModel: function (sourceId) { Module._ifcv_unload_model_c(sourceId | 0); },
|
||||
loadModel: function (sourceId) { return Module._ifcv_load_model_c(sourceId | 0) !== 0; },
|
||||
modelUnloaded: function (sourceId) { return Module._ifcv_model_unloaded_c(sourceId | 0) !== 0; },
|
||||
modelVramBytes: function (sourceId) { return Module._ifcv_model_vram_bytes_c(sourceId | 0); },
|
||||
|
||||
registerFileSource: registerFile,
|
||||
registerUrlSource: registerUrl,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user