ifcviewer-web: never hand setBindGroup the whole wasm heap as dynamic offsets

Emscripten's generated WebGPU shim implements the dynamic-offset path of
wgpuRenderPassEncoderSetBindGroup as

    pass.setBindGroup(index, group, HEAPU32, ptr >>> 2, count);

where HEAPU32 is the view over the entire wasm linear memory. Browsers
validate the byte length of that whole backing buffer, not the slice
actually read, and refuse anything over 2 GB. This build lets the heap
grow to 4 GB because large federations need it, so on a big enough
session (66 models) every dynamic-offset draw — the axis gizmo, section
gizmo and overlay lines, all drawn every frame — throws

    TypeError: GPURenderPassEncoder.setBindGroup: Argument 3 can't be an
    ArrayBuffer or an ArrayBufferView larger than 2 GB

on every frame for the life of the page.

ifcviewer::setBindGroupDynamic copies the handful of offsets into a
small Uint32Array on web (HEAPU32.slice, not subarray, which would alias
the heap again) and forwards straight through natively. The five
dynamic-offset call sites route through it. A Playwright spec spies on
setBindGroup and asserts the largest buffer it is ever handed is the
offsets themselves (4 bytes), where the shim previously passed the full
268 MB heap 35 times in three seconds of idle rendering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-08-23 22:20:14 +10:00
parent b7f1f1728f
commit 06d87e21a7
7 changed files with 226 additions and 5 deletions
+4 -3
View File
@@ -18,6 +18,7 @@
********************************************************************************/
#include "AxisIndicatorRenderer.h"
#include "WgpuDynamicOffsets.h"
#include <algorithm>
#include <cmath>
@@ -341,10 +342,10 @@ void AxisIndicatorRenderer::encodePivot(WGPURenderPassEncoder pass,
wgpuRenderPassEncoderSetVertexBuffer(pass, 0, vertex_buffer_, 0, WGPU_WHOLE_SIZE);
wgpuRenderPassEncoderSetPipeline(pass, pivot_xray_pipeline_);
wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 1, &xray_off);
ifcviewer::setBindGroupDynamic(pass, 0, bind_group_, 1, &xray_off);
wgpuRenderPassEncoderDraw(pass, kAxisVertexCount, 1, 0, 0);
wgpuRenderPassEncoderSetPipeline(pass, pivot_pipeline_);
wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 1, &visible_off);
ifcviewer::setBindGroupDynamic(pass, 0, bind_group_, 1, &visible_off);
wgpuRenderPassEncoderDraw(pass, kAxisVertexCount, 1, 0, 0);
}
@@ -404,7 +405,7 @@ void AxisIndicatorRenderer::encodeCornerAxis(WGPUCommandEncoder enc,
0.0f, 1.0f);
wgpuRenderPassEncoderSetPipeline(pass, corner_pipeline_);
wgpuRenderPassEncoderSetVertexBuffer(pass, 0, vertex_buffer_, 0, WGPU_WHOLE_SIZE);
wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 1, &slot_offset);
ifcviewer::setBindGroupDynamic(pass, 0, bind_group_, 1, &slot_offset);
wgpuRenderPassEncoderDraw(pass, kAxisVertexCount, 1, 0, 0);
wgpuRenderPassEncoderEnd(pass);
wgpuRenderPassEncoderRelease(pass);
+1
View File
@@ -154,6 +154,7 @@ set(IFCVIEWER_CORE_SOURCES
StreamingThread.cpp
SectionGizmoRenderer.cpp
ViewportCore.cpp
WgpuDynamicOffsets.cpp
)
# Web needs a zstd DECODER (Emscripten has no zstd port; the desktop links the
# full libzstd below). Rather than vendor a generated blob, fetch the pinned
+2 -1
View File
@@ -18,6 +18,7 @@
********************************************************************************/
#include "OverlayRenderer.h"
#include "WgpuDynamicOffsets.h"
#include <QFont>
#include <QFontMetrics>
@@ -886,7 +887,7 @@ void OverlayRenderer::encodeOverlayLines(WGPURenderPassEncoder pass,
wgpuQueueWriteBuffer(queue_, overlay_line_uniform_buffer_,
slot_off + 96, viewport, sizeof(viewport));
const uint32_t dynamic_offsets[1] = { uint32_t(slot_off) };
wgpuRenderPassEncoderSetBindGroup(pass, 0, overlay_line_bind_group_,
ifcviewer::setBindGroupDynamic(pass, 0, overlay_line_bind_group_,
1, dynamic_offsets);
wgpuRenderPassEncoderDraw(pass, d.vertex_count, 1, d.first_vertex, 0);
}
+2 -1
View File
@@ -18,6 +18,7 @@
********************************************************************************/
#include "SectionGizmoRenderer.h"
#include "WgpuDynamicOffsets.h"
#include <algorithm>
#include <array>
@@ -335,7 +336,7 @@ void SectionGizmoRenderer::encode(WGPURenderPassEncoder pass, const Eigen::Matri
bitangent, nn, tr, tg, tb, 1.0f, vw, vh);
const uint32_t slot_offset = uint32_t(i) * kSectionUniformSlot;
wgpuQueueWriteBuffer(queue_, uniform_buffer_, slot_offset, slot, sizeof(slot));
wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 1, &slot_offset);
ifcviewer::setBindGroupDynamic(pass, 0, bind_group_, 1, &slot_offset);
wgpuRenderPassEncoderDraw(pass, uint32_t(vertex_count_), 1, 0, 0);
}
}
+65
View File
@@ -0,0 +1,65 @@
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#include "WgpuDynamicOffsets.h"
#ifdef __EMSCRIPTEN__
#include <emscripten/em_js.h>
#endif
namespace ifcviewer {
#ifdef __EMSCRIPTEN__
namespace {
EM_JS(void, ifcv_set_bind_group_dynamic_js,
(WGPURenderPassEncoder pass, uint32_t group_index, WGPUBindGroup group,
uint32_t offsets_ptr, uint32_t count), {
// HEAPU32.slice() copies into a freshly allocated buffer of exactly
// `count` elements; .subarray() would alias the whole heap again and
// reintroduce the bug this function exists to avoid.
var start = offsets_ptr >>> 2;
var small = HEAPU32.slice(start, start + count);
WebGPU.getJsObject(pass).setBindGroup(
group_index, WebGPU.getJsObject(group), small, 0, count);
});
} // namespace
void setBindGroupDynamic(WGPURenderPassEncoder pass, uint32_t group_index,
WGPUBindGroup group, uint32_t count,
const uint32_t* offsets) {
if (count == 0) {
wgpuRenderPassEncoderSetBindGroup(pass, group_index, group, 0, nullptr);
return;
}
ifcv_set_bind_group_dynamic_js(pass, group_index, group,
uint32_t(reinterpret_cast<uintptr_t>(offsets)), count);
}
#else
void setBindGroupDynamic(WGPURenderPassEncoder pass, uint32_t group_index,
WGPUBindGroup group, uint32_t count,
const uint32_t* offsets) {
wgpuRenderPassEncoderSetBindGroup(pass, group_index, group, count, offsets);
}
#endif
} // namespace ifcviewer
+61
View File
@@ -0,0 +1,61 @@
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#ifndef WGPUDYNAMICOFFSETS_H
#define WGPUDYNAMICOFFSETS_H
#include <cstdint>
#include <webgpu/webgpu.h>
namespace ifcviewer {
// setBindGroup with dynamic offsets. Always use this instead of calling
// wgpuRenderPassEncoderSetBindGroup with a nonzero offset count.
//
// Emscripten's generated WebGPU shim implements the dynamic-offset path as
//
// pass.setBindGroup(index, group, HEAPU32, ptr >>> 2, count);
//
// where HEAPU32 is the persistent view over the *entire* wasm linear memory.
// Browsers validate the byte length of the whole backing buffer handed to
// setBindGroup, not the (start, length) slice actually read, and reject
// anything over 2 GB:
//
// TypeError: GPURenderPassEncoder.setBindGroup: Argument 3 can't be an
// ArrayBuffer or an ArrayBufferView larger than 2 GB
//
// So once the heap grows past 2^31 bytes every dynamic-offset draw throws, on
// every frame, for the life of the page -- and this build deliberately allows
// that (ALLOW_MEMORY_GROWTH with MAXIMUM_MEMORY=4 GB, because large
// federations need the headroom). The offsets are only a handful of uint32_t,
// so the web implementation copies them into a small short-lived Uint32Array.
// Native builds forward straight through; wgpu-native reads the pointer
// directly and has no such limit.
//
// Defined out-of-line in WgpuDynamicOffsets.cpp: the web path is an EM_JS
// function, and EM_JS emits real per-translation-unit symbols that collide at
// link time if instantiated in more than one TU.
void setBindGroupDynamic(WGPURenderPassEncoder pass, uint32_t group_index,
WGPUBindGroup group, uint32_t count,
const uint32_t* offsets);
} // namespace ifcviewer
#endif