mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 07:49:59 +00:00
ifcviewer: pack the cull-hot instance fields and skip unchanged culls
On the single-threaded web build the CPU cull WAS the frame: 52-60 ms of a 60 ms frame at 640k instances (desktop hides the same cost across cores via std::async, which web cannot use without COOP/COEP+pthreads). Two changes, both also helping desktop: - ModelGpuData::CullInstance packs the six AABB floats and three ids the cull reads into 40 contiguous bytes. InstanceInfo is 232 bytes with the AABB 200 bytes away from the ids, so the walk paid two or three cache lines per instance. Rebuilt by rebuildCullInstances at model apply and inside uploadInstanceRecords, which every recompose, transform and colour-override change already funnels through. Measured on web: 94 ns/instance -> 36 ns/instance during a continuous orbit (~2.6x). - render() re-culls only when a cull input changed: the camera, a cull-relevant setting (contribution px, LOD px, x-ray, HiZ on/off), a fresh HiZ pyramid, or scene_epoch_ — bumped by chunk residency, visibility, colours, transforms, model add/remove/hide/unload. A frame requested for an overlay redraw, pick feedback, or a streaming tick where nothing landed draws from the buffers the last cull uploaded and skips the walk entirely. Benchmarks are exempt so bench numbers keep measuring the real cull. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -395,6 +395,25 @@ struct ModelGpuData {
|
||||
std::vector<MeshInfo> meshes;
|
||||
std::vector<InstanceInfo> instances;
|
||||
|
||||
// The cull-hot per-instance fields packed contiguously. InstanceInfo is
|
||||
// 232 bytes with the AABB 200 bytes from the ids, so the per-frame cull
|
||||
// paid two or three cache lines per instance — at half a million
|
||||
// instances that is the whole frame budget on the single-threaded web
|
||||
// build. 40 bytes per entry here makes the walk sequential. Rebuilt by
|
||||
// rebuildCullInstances wherever instances change (applyCachedModel,
|
||||
// uploadInstanceRecords — which every recompose and colour change
|
||||
// already funnels through).
|
||||
struct CullInstance {
|
||||
float aabb_min[3];
|
||||
float aabb_max[3];
|
||||
std::uint32_t mesh_id;
|
||||
std::uint32_t object_id;
|
||||
std::uint32_t color_override_rgba8;
|
||||
std::uint32_t chunk_idx;
|
||||
};
|
||||
static_assert(sizeof(CullInstance) == 40, "keep the cull walk dense");
|
||||
std::vector<CullInstance> cull_instances;
|
||||
|
||||
// Per-mesh "any vertex has alpha < 255?" flag, indexed by mesh_id.
|
||||
// Populated at uploadStreamedMesh / applyStreamedChunk as vertex bytes
|
||||
// become CPU-resident. Used at cull time to classify each instance
|
||||
@@ -501,5 +520,7 @@ void releaseWgpuModelGpuData(ModelGpuData& m, BufferPool& pool);
|
||||
// and the per-chunk cull buffers. Chunk bookkeeping is left intact so the
|
||||
// buffers can be re-created — the undo step of a failed model load.
|
||||
void releaseModelBuffers(ModelGpuData& m);
|
||||
// Refresh ModelGpuData::cull_instances from instances + instance_chunk_idx.
|
||||
void rebuildCullInstances(ModelGpuData& m);
|
||||
|
||||
#endif // WGPUMODELGPUDATA_H
|
||||
|
||||
@@ -143,6 +143,7 @@ std::vector<InstanceGpu> instanceGpuRecords(const std::vector<InstanceInfo>& ins
|
||||
} // namespace
|
||||
|
||||
void ViewportCore::removeModel(uint32_t session_model_id) {
|
||||
markCullInputsChanged();
|
||||
auto it = models_gpu_.find(session_model_id);
|
||||
if (it == models_gpu_.end()) return;
|
||||
releaseWgpuModelGpuData(it->second, pool_);
|
||||
@@ -151,6 +152,7 @@ void ViewportCore::removeModel(uint32_t session_model_id) {
|
||||
}
|
||||
|
||||
void ViewportCore::resetScene() {
|
||||
markCullInputsChanged();
|
||||
for (auto& [session_model_id, m] : models_gpu_) releaseWgpuModelGpuData(m, pool_);
|
||||
models_gpu_.clear();
|
||||
// A fresh scene should auto-frame its first model. Without this the flag
|
||||
@@ -162,6 +164,7 @@ void ViewportCore::resetScene() {
|
||||
}
|
||||
|
||||
void ViewportCore::hideModel(uint32_t session_model_id) {
|
||||
markCullInputsChanged();
|
||||
auto it = models_gpu_.find(session_model_id);
|
||||
if (it == models_gpu_.end() || it->second.hidden) return;
|
||||
it->second.hidden = true;
|
||||
@@ -169,6 +172,7 @@ void ViewportCore::hideModel(uint32_t session_model_id) {
|
||||
}
|
||||
|
||||
void ViewportCore::showModel(uint32_t session_model_id) {
|
||||
markCullInputsChanged();
|
||||
auto it = models_gpu_.find(session_model_id);
|
||||
if (it == models_gpu_.end() || !it->second.hidden) return;
|
||||
it->second.hidden = false;
|
||||
@@ -186,6 +190,7 @@ void ViewportCore::unloadModel(uint32_t session_model_id) {
|
||||
}
|
||||
|
||||
bool ViewportCore::loadModel(uint32_t session_model_id) {
|
||||
markCullInputsChanged();
|
||||
auto it = models_gpu_.find(session_model_id);
|
||||
if (it == models_gpu_.end()) return false;
|
||||
ModelGpuData& m = it->second;
|
||||
@@ -335,7 +340,28 @@ float ViewportCore::chunkScreenAreaPx(const ModelGpuData::Chunk& c,
|
||||
return (xmax - xmin) * (ymax - ymin);
|
||||
}
|
||||
|
||||
void rebuildCullInstances(ModelGpuData& m) {
|
||||
m.cull_instances.resize(m.instances.size());
|
||||
for (std::size_t i = 0; i < m.instances.size(); ++i) {
|
||||
const InstanceInfo& inst = m.instances[i];
|
||||
ModelGpuData::CullInstance& out = m.cull_instances[i];
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
out.aabb_min[a] = inst.world_aabb_min[a];
|
||||
out.aabb_max[a] = inst.world_aabb_max[a];
|
||||
}
|
||||
out.mesh_id = inst.mesh_id;
|
||||
out.object_id = inst.object_id;
|
||||
out.color_override_rgba8 = inst.color_override_rgba8;
|
||||
out.chunk_idx = i < m.instance_chunk_idx.size()
|
||||
? m.instance_chunk_idx[i] : 0;
|
||||
}
|
||||
}
|
||||
|
||||
void ViewportCore::uploadInstanceRecords(ModelGpuData& m) {
|
||||
// Every path that mutates instances (recompose, transforms, colour
|
||||
// overrides) funnels through here, so the packed cull mirror follows.
|
||||
rebuildCullInstances(m);
|
||||
markCullInputsChanged();
|
||||
if (!wgpu_initialized_ || m.instances.empty() || m.instance_storage == nullptr) return;
|
||||
const std::vector<InstanceGpu> gpu = instanceGpuRecords(m.instances);
|
||||
wgpuQueueWriteBuffer(queue_, m.instance_storage, 0,
|
||||
@@ -2352,6 +2378,7 @@ bool ViewportCore::applyStreamedChunk(
|
||||
c.is_resident = true;
|
||||
c.is_loading = false;
|
||||
c.loaded_frame_idx = streaming_frame_idx_;
|
||||
markCullInputsChanged();
|
||||
|
||||
// The CPU triangle shadow follows residency: count this chunk into each
|
||||
// of its meshes (a mesh can live in several chunks under the spatial
|
||||
@@ -2508,6 +2535,7 @@ void ViewportCore::unloadChunk(ModelGpuData& m, std::size_t chunk_idx) {
|
||||
c.total_visible_draws = 0;
|
||||
c.total_visible_vertices = 0;
|
||||
c.is_resident = false;
|
||||
markCullInputsChanged();
|
||||
|
||||
// CPU side of the eviction. The cull scratch and the uploaded mirrors
|
||||
// are only meaningful for a resident chunk (cull no longer emits for
|
||||
@@ -3133,7 +3161,8 @@ std::uint32_t ViewportCore::cullModelCpuCompute(
|
||||
const HizOccludedFn& hiz_occluded) const {
|
||||
std::uint32_t hiz_rejects = 0;
|
||||
|
||||
if (m.instances.empty() || m.meshes.empty() || m.chunks.empty()) {
|
||||
if (m.instances.empty() || m.meshes.empty() || m.chunks.empty()
|
||||
|| m.cull_instances.size() != m.instances.size()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -3161,15 +3190,15 @@ std::uint32_t ViewportCore::cullModelCpuCompute(
|
||||
std::vector<std::uint32_t> running_vertex_count(m.chunks.size(), 0);
|
||||
|
||||
auto process_instance = [&](std::uint32_t i) {
|
||||
const auto& inst = m.instances[i];
|
||||
const ModelGpuData::CullInstance& inst = m.cull_instances[i];
|
||||
if (inst.mesh_id >= m.meshes.size()) return;
|
||||
if (visibility_.isHidden(inst.object_id)) return;
|
||||
// Per-instance frustum still needed: a partially-covered subtree
|
||||
// descended this far means *some* leaves are visible, but not
|
||||
// necessarily this one.
|
||||
if (!aabbInFrustum(inst.world_aabb_min, inst.world_aabb_max, planes)) return;
|
||||
if (!aabbInFrustum(inst.aabb_min, inst.aabb_max, planes)) return;
|
||||
|
||||
const std::uint32_t chunk_idx = m.instance_chunk_idx[i];
|
||||
const std::uint32_t chunk_idx = inst.chunk_idx;
|
||||
ModelGpuData::Chunk& c = m.chunks[chunk_idx];
|
||||
|
||||
// Bump the chunk's frustum-only counter before contribution / HiZ
|
||||
@@ -3183,12 +3212,12 @@ std::uint32_t ViewportCore::cullModelCpuCompute(
|
||||
// rectangle projection (tight — used for streaming priority).
|
||||
float projected_px = std::numeric_limits<float>::infinity();
|
||||
{
|
||||
const float cx = 0.5f * (inst.world_aabb_min[0] + inst.world_aabb_max[0]);
|
||||
const float cy = 0.5f * (inst.world_aabb_min[1] + inst.world_aabb_max[1]);
|
||||
const float cz = 0.5f * (inst.world_aabb_min[2] + inst.world_aabb_max[2]);
|
||||
const float ex = inst.world_aabb_max[0] - inst.world_aabb_min[0];
|
||||
const float ey = inst.world_aabb_max[1] - inst.world_aabb_min[1];
|
||||
const float ez = inst.world_aabb_max[2] - inst.world_aabb_min[2];
|
||||
const float cx = 0.5f * (inst.aabb_min[0] + inst.aabb_max[0]);
|
||||
const float cy = 0.5f * (inst.aabb_min[1] + inst.aabb_max[1]);
|
||||
const float cz = 0.5f * (inst.aabb_min[2] + inst.aabb_max[2]);
|
||||
const float ex = inst.aabb_max[0] - inst.aabb_min[0];
|
||||
const float ey = inst.aabb_max[1] - inst.aabb_min[1];
|
||||
const float ez = inst.aabb_max[2] - inst.aabb_min[2];
|
||||
const float radius_world = 0.5f * std::sqrt(ex*ex + ey*ey + ez*ez);
|
||||
const float view_z = forward[0] * (cx - eye[0])
|
||||
+ forward[1] * (cy - eye[1])
|
||||
@@ -3230,7 +3259,7 @@ std::uint32_t ViewportCore::cullModelCpuCompute(
|
||||
if (!c.is_resident) return;
|
||||
|
||||
if (hiz_active
|
||||
&& hiz_occluded(inst.world_aabb_min, inst.world_aabb_max)) {
|
||||
&& hiz_occluded(inst.aabb_min, inst.aabb_max)) {
|
||||
++hiz_rejects;
|
||||
return;
|
||||
}
|
||||
@@ -3744,6 +3773,8 @@ void ViewportCore::applyCachedModel(std::uint32_t session_model_id,
|
||||
for (std::uint32_t i = 0; i < std::uint32_t(model_gpu_data.instances.size()); ++i) {
|
||||
model_gpu_data.object_id_to_instance.emplace(model_gpu_data.instances[i].object_id, i);
|
||||
}
|
||||
rebuildCullInstances(model_gpu_data);
|
||||
markCullInputsChanged();
|
||||
|
||||
// Per-chunk world AABBs + instance-id lists from instance_to_chunk.
|
||||
for (std::size_t chunk_index = 0; chunk_index < model_gpu_data.chunks.size(); ++chunk_index) {
|
||||
@@ -5074,6 +5105,7 @@ void ViewportCore::drainHizReadbacks() {
|
||||
|
||||
hiz_vp_ = hiz_slot_vp_[slot];
|
||||
hiz_valid_ = true;
|
||||
markCullInputsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6468,6 +6500,7 @@ void ViewportCore::applyMarqueeToSelection(const std::vector<std::uint32_t>& ids
|
||||
}
|
||||
|
||||
void ViewportCore::hideSelected() {
|
||||
markCullInputsChanged();
|
||||
if (selection_.count() == 0) return;
|
||||
for (uint32_t id : selection_.selectionIds()) visibility_.hide(id);
|
||||
const size_t n = selection_.count();
|
||||
@@ -6477,6 +6510,7 @@ void ViewportCore::hideSelected() {
|
||||
}
|
||||
|
||||
void ViewportCore::isolateSelected() {
|
||||
markCullInputsChanged();
|
||||
if (selection_.count() == 0) return;
|
||||
// Hide every object in a VISIBLE model that isn't selected. Model-hidden
|
||||
// objects stay model-hidden (element-level hiding on top is redundant), and
|
||||
@@ -6495,6 +6529,7 @@ void ViewportCore::isolateSelected() {
|
||||
}
|
||||
|
||||
void ViewportCore::showAll() {
|
||||
markCullInputsChanged();
|
||||
if (visibility_.hiddenCount() == 0) return;
|
||||
visibility_.clear();
|
||||
Log::info() << "[wgpu] show all";
|
||||
@@ -6502,6 +6537,7 @@ void ViewportCore::showAll() {
|
||||
}
|
||||
|
||||
void ViewportCore::hideAll() {
|
||||
markCullInputsChanged();
|
||||
// Element-level hide of everything in a visible model — the inverse of
|
||||
// showAll, and isolateSelected with an empty selection. Model-hidden
|
||||
// models are already gone from the cull, so they contribute nothing.
|
||||
@@ -6514,6 +6550,7 @@ void ViewportCore::hideAll() {
|
||||
}
|
||||
|
||||
void ViewportCore::setObjectsVisible(const std::vector<std::uint32_t>& object_ids, bool visible) {
|
||||
markCullInputsChanged();
|
||||
for (std::uint32_t id : object_ids) {
|
||||
if (visible) visibility_.show(id);
|
||||
else visibility_.hide(id);
|
||||
@@ -7793,10 +7830,6 @@ void ViewportCore::render() {
|
||||
updateFrameUniforms();
|
||||
|
||||
// ---- Per-frame cull --------------------------------------------------
|
||||
last_visible_objects_ = 0;
|
||||
last_visible_triangles_ = 0;
|
||||
last_sub_draws_ = 0;
|
||||
hiz_reject_count_ = 0;
|
||||
Stopwatch cull_timer;
|
||||
cull_timer.start();
|
||||
cull_writes_this_frame_ = 0;
|
||||
@@ -7877,6 +7910,36 @@ void ViewportCore::render() {
|
||||
};
|
||||
}
|
||||
|
||||
// Re-cull only when something that can change its outcome did:
|
||||
// the camera, the scene epoch (residency / visibility / colours /
|
||||
// transforms / models / a new HiZ pyramid), or a cull-relevant
|
||||
// setting. Everything the draw needs from a cull persists — the
|
||||
// per-chunk counters and the uploaded visible_draws buffers — so a
|
||||
// frame requested for an overlay, a pick highlight, or a streaming
|
||||
// tick where nothing landed skips the whole walk. Benchmarks are
|
||||
// exempt so bench numbers keep measuring the real cull.
|
||||
const bool cull_inputs_unchanged =
|
||||
bench_total_ == 0
|
||||
&& has_last_cull_
|
||||
&& last_cull_epoch_ == scene_epoch_
|
||||
&& last_cull_vp_ == vp_this_frame
|
||||
&& last_cull_min_px_ == effective_min_px
|
||||
&& last_cull_lod_px_ == lod1_pixel_threshold_
|
||||
&& last_cull_xray_ == xray_alpha_cap_
|
||||
&& last_cull_hiz_ == hiz_for_this_frame;
|
||||
if (!cull_inputs_unchanged) {
|
||||
has_last_cull_ = true;
|
||||
last_cull_epoch_ = scene_epoch_;
|
||||
last_cull_vp_ = vp_this_frame;
|
||||
last_cull_min_px_ = effective_min_px;
|
||||
last_cull_lod_px_ = lod1_pixel_threshold_;
|
||||
last_cull_xray_ = xray_alpha_cap_;
|
||||
last_cull_hiz_ = hiz_for_this_frame;
|
||||
last_visible_objects_ = 0;
|
||||
last_visible_triangles_ = 0;
|
||||
last_sub_draws_ = 0;
|
||||
hiz_reject_count_ = 0;
|
||||
|
||||
// Force sequential on Emscripten: std::async(std::launch::async)
|
||||
// without -pthread throws std::system_error from inside libstdc++,
|
||||
// and we link without exceptions so that becomes abort(). Until
|
||||
@@ -7929,6 +7992,10 @@ void ViewportCore::render() {
|
||||
}
|
||||
last_cull_compute_ms_ = cull_compute_ms;
|
||||
last_cull_upload_ms_ = double(upload_timer.nsecsElapsed()) / 1e6;
|
||||
} else {
|
||||
last_cull_compute_ms_ = 0.0;
|
||||
last_cull_upload_ms_ = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
const double cull_only_ms = double(cull_timer.nsecsElapsed()) / 1e6;
|
||||
|
||||
@@ -1594,6 +1594,27 @@ private:
|
||||
// for parallel-vs-serial benchmarking. Default ON.
|
||||
bool cull_threads_enabled_ = true;
|
||||
|
||||
// ---- Cull-input tracking -------------------------------------------
|
||||
//
|
||||
// The CPU cull is the single largest per-frame cost (the whole frame on
|
||||
// the single-threaded web build), and most requested frames do not
|
||||
// change its inputs — overlay redraws, pick feedback, streaming frames
|
||||
// where no chunk actually landed. scene_epoch_ is bumped by everything
|
||||
// that can alter a cull's outcome besides the camera (residency,
|
||||
// visibility, colours, transforms, model set, HiZ pyramid updates);
|
||||
// render() re-culls only when the epoch, the camera, or a cull-relevant
|
||||
// setting changed, and otherwise draws from the buffers the last cull
|
||||
// uploaded.
|
||||
std::uint64_t scene_epoch_ = 0;
|
||||
void markCullInputsChanged() { ++scene_epoch_; }
|
||||
bool has_last_cull_ = false;
|
||||
Eigen::Matrix4f last_cull_vp_ = Eigen::Matrix4f::Zero();
|
||||
std::uint64_t last_cull_epoch_ = 0;
|
||||
float last_cull_min_px_ = -1.0f;
|
||||
float last_cull_lod_px_ = -1.0f;
|
||||
float last_cull_xray_ = -1.0f;
|
||||
bool last_cull_hiz_ = false;
|
||||
|
||||
// Per-frame stats latched by render() for FrameStats emission +
|
||||
// the interactive heartbeat / bench per-frame line.
|
||||
std::uint32_t last_visible_objects_ = 0;
|
||||
|
||||
Reference in New Issue
Block a user