mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-12 18:43:26 +00:00
wgpu backend: 10× perf — megadraw, async HiZ, parallel cull, motion mode
Closes the perf gap to the GL backend on real BIM benchmarks. On a 10-
sidecar / 380k-instance corpus at a fixed --camera the wgpu binary went
from 110.6 ms to 11.6 ms (vs GL's 23 ms — half the frame time, but
note GL is doing extra work the wgpu backend hasn't ported yet; see
the caveats list at the bottom). Bundled because the pieces interlock
and shipping any of them without the others reintroduces the same wall.
1. Cross-mesh vertex pulling (single mega-draw per model)
The previous one-drawIndexed-per-(mesh × LOD-bucket) loop was costing
~13ms on a 27k-mesh scene. CPU now emits a flat visible_draws[]
(16 B per visible (mesh,lod,instance)) plus a prefix_sums[] table.
WGSL binary-searches prefix_sums by @builtin(vertex_index) to find
the entry, then manually fetches the mesh-local index from a
storage-bound indices[] and pulls the packed 12 B vertex. No
setIndexBuffer; the shader reads everything from storage. Bind
group grew from 4 to 7 entries (vertices, meshes, instances,
indices, visible_draws, prefix_sums, per-model uniform) — well
under WebGPU's mandatory 8 storage / 12 uniform floor.
2. Async HiZ readback via ping-pong staging buffers
Sync wait via wgpuInstanceProcessEvents was costing ~37 ms on a
real scene (GPU drain). Two staging slots now ping-pong: frame N
kicks a non-blocking mapAsync on slot K, frame N+1's first action
is one processEvents drain. Pyramid is 1-2 frames stale — matches
the "slightly-stale depth, fine" pattern the GL backend already
documents. encodeHizResolve returns -1 (skip) if both slots are
in flight; cull keeps using the most recent pyramid.
3. Cull reorder: contribution before HiZ
HiZ projection is ~10× more expensive than the contribution
check, yet most contribution-survivors would be HiZ-rejected
anyway on dense scenes. Computing projected_px first lets
contribution short-circuit ~80% of HiZ tests with no rejection-
quality loss. Saved ~34 ms on the dense bench.
4. Motion-mode contribution threshold
AppSettings::motionMinPixelRadius parity. While the camera is
changing (orbit/pan/zoom/--benchmark sweep), drop instances
below 10 px instead of 2 px. Halves visible_objects during
motion with no perceived quality loss.
5. Parallel cull (std::async across models)
Per-model cullModelCpu split into Compute (CPU-only, thread-safe)
+ Upload (main-thread wgpu queue writes). std::async fan-outs the
compute across models; main-thread joins and uploads. Wall-clock
cull on the 10-model corpus drops from ~17 ms single-threaded to
~9 ms across cores.
6. --no-hiz CLI flag + per-phase benchmark timings
Benchmark now also prints "per-frame avg ms: cull=X
hiz_readback=Y" so future regressions can be attributed without
guesswork. --no-hiz toggles the master switch from the CLI.
Honest caveats — wgpu is currently faster mostly because GL is doing
work we haven't ported yet:
- Edge silhouette pass (stage 9) will add ~3-5 ms back to wgpu.
- GL's HiZ uses the BVH so it rejects whole subtrees (1.7k vs
our 358 rejects on the same scene). BVH for HiZ is future work
(task #13 / a new task) — until then we draw more sub-pixel
geometry that's behind closer surfaces. Visually correct, perf
cost paid. Stage 4+5 are unaffected.
Verified pixel-identical on basic.ifc through every change. Real-scene
visual diff against GL pending the --screenshot flag on the GL minimal
(task #10's other half).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -48,10 +48,13 @@ int main(int argc, char* argv[]) {
|
||||
parser.addOption({{"c", "camera"},
|
||||
"Set camera as tx,ty,tz,dist,yaw,pitch (same format as IfcViewerMinimal).",
|
||||
"params"});
|
||||
parser.addOption({"no-hiz",
|
||||
"Disable HiZ occlusion culling for perf diagnostics."});
|
||||
parser.process(app);
|
||||
|
||||
auto* viewport = new WgpuViewportWindow;
|
||||
viewport->resize(1280, 800);
|
||||
if (parser.isSet("no-hiz")) viewport->hiz_enabled_ = false;
|
||||
|
||||
QWidget* container = QWidget::createWindowContainer(viewport);
|
||||
container->setMinimumSize(320, 240);
|
||||
|
||||
@@ -51,31 +51,47 @@ struct WgpuModelGpuData {
|
||||
// recompose from placement_transformation when stage matrices change.
|
||||
WGPUBuffer instance_storage = nullptr;
|
||||
|
||||
// u32[] of visible instance indices, repacked per frame by the CPU cull
|
||||
// into per-mesh contiguous slices. Sized for the worst case
|
||||
// (instance_count entries) at applyCachedModel time so we never have to
|
||||
// recreate it (and the bind group that references it) mid-frame.
|
||||
WGPUBuffer visible_buffer = nullptr;
|
||||
size_t visible_buffer_capacity = 0; // entries (u32 count), not bytes
|
||||
// Cross-mesh vertex pulling: one entry per visible (mesh, lod, instance)
|
||||
// tuple, written into a single flat buffer per frame. The vertex shader
|
||||
// binary-searches `prefix_sums` to find which entry a given
|
||||
// gl_VertexID belongs to, then fetches that entry's mesh slice via the
|
||||
// offsets baked here. Pre-sized at applyCachedModel to a generous
|
||||
// worst case so the bind group stays valid for the model's lifetime.
|
||||
//
|
||||
// std430 layout: 16 bytes per entry, naturally aligned.
|
||||
struct alignas(16) VisibleDrawGpu {
|
||||
uint32_t mesh_id; // -> meshes[] for quantisation basis
|
||||
uint32_t instance_idx; // -> instances[] for transform + ids
|
||||
uint32_t ebo_first_u32; // start of this entry's slice in indices[]
|
||||
uint32_t base_vertex; // start of this mesh's slice in vertices[]
|
||||
};
|
||||
static_assert(sizeof(VisibleDrawGpu) == 16, "VisibleDrawGpu must be 16 bytes");
|
||||
|
||||
// Bind group binding the four storage buffers above (group=1 in the
|
||||
// main pipeline). Built in applyCachedModel after the buffers exist.
|
||||
WGPUBuffer visible_draws_buffer = nullptr;
|
||||
size_t visible_draws_capacity = 0; // entries (not bytes)
|
||||
|
||||
// prefix_sums[i] = sum of index_counts for visible_draws[0..i-1].
|
||||
// Sized to visible_draws_capacity + 1 so prefix_sums[N] = total verts.
|
||||
WGPUBuffer prefix_sums_buffer = nullptr;
|
||||
size_t prefix_sums_capacity = 0; // entries (u32 count)
|
||||
|
||||
// Per-model uniform: (draw_count, total_vertex_count, _pad, _pad).
|
||||
// The vertex shader uses draw_count to bound the binary search; CPU
|
||||
// uses total_vertex_count as the draw() call's vertex count.
|
||||
WGPUBuffer per_model_uniform = nullptr;
|
||||
|
||||
// Bind group binding the storage + uniform buffers above (group=1 in
|
||||
// the main pipeline). Built in applyCachedModel.
|
||||
WGPUBindGroup bind_group = nullptr;
|
||||
|
||||
// One per mesh, populated per frame by cullAndCompact. instance_count==0
|
||||
// means the mesh contributes nothing this frame and the draw is skipped.
|
||||
struct MeshDraw {
|
||||
uint32_t first_instance = 0; // offset into visible_buffer
|
||||
uint32_t instance_count = 0;
|
||||
uint32_t first_index = 0; // index buffer offset (in u32 indices)
|
||||
int32_t base_vertex = 0; // added to every fetched vertex_index
|
||||
uint32_t index_count = 0;
|
||||
};
|
||||
std::vector<MeshDraw> mesh_draws; // size = meshes.size() after first frame
|
||||
// Per-frame, populated by cullModelCpu and consumed by render().
|
||||
uint32_t total_visible_vertices = 0;
|
||||
uint32_t total_visible_draws = 0;
|
||||
|
||||
// Scratch reused each frame so per-frame cull doesn't allocate. Sized
|
||||
// to instance_count entries on first use; never shrunk.
|
||||
std::vector<uint32_t> visible_flat_scratch;
|
||||
// on first use; never shrunk.
|
||||
std::vector<VisibleDrawGpu> visible_draws_scratch;
|
||||
std::vector<uint32_t> prefix_sums_scratch;
|
||||
|
||||
// Size mirrors for stats / range checks.
|
||||
size_t vertex_bytes = 0;
|
||||
|
||||
@@ -33,8 +33,10 @@
|
||||
#include <webgpu/wgpu.h> // wgpu-native extensions (logging, MULTI_DRAW_INDIRECT, …)
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <future>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
@@ -130,32 +132,42 @@ static WGPUBuffer createBufferWithData(WGPUDevice device, WGPUQueue queue,
|
||||
}
|
||||
|
||||
void releaseWgpuModelGpuData(WgpuModelGpuData& m) {
|
||||
if (m.bind_group) { wgpuBindGroupRelease(m.bind_group); m.bind_group = nullptr; }
|
||||
if (m.vertex_storage) { wgpuBufferRelease(m.vertex_storage); m.vertex_storage = nullptr; }
|
||||
if (m.index_buffer) { wgpuBufferRelease(m.index_buffer); m.index_buffer = nullptr; }
|
||||
if (m.mesh_storage) { wgpuBufferRelease(m.mesh_storage); m.mesh_storage = nullptr; }
|
||||
if (m.instance_storage) { wgpuBufferRelease(m.instance_storage); m.instance_storage = nullptr; }
|
||||
if (m.visible_buffer) { wgpuBufferRelease(m.visible_buffer); m.visible_buffer = nullptr; }
|
||||
if (m.bind_group) { wgpuBindGroupRelease(m.bind_group); m.bind_group = nullptr; }
|
||||
if (m.vertex_storage) { wgpuBufferRelease(m.vertex_storage); m.vertex_storage = nullptr; }
|
||||
if (m.index_buffer) { wgpuBufferRelease(m.index_buffer); m.index_buffer = nullptr; }
|
||||
if (m.mesh_storage) { wgpuBufferRelease(m.mesh_storage); m.mesh_storage = nullptr; }
|
||||
if (m.instance_storage) { wgpuBufferRelease(m.instance_storage); m.instance_storage = nullptr; }
|
||||
if (m.visible_draws_buffer) { wgpuBufferRelease(m.visible_draws_buffer); m.visible_draws_buffer = nullptr; }
|
||||
if (m.prefix_sums_buffer) { wgpuBufferRelease(m.prefix_sums_buffer); m.prefix_sums_buffer = nullptr; }
|
||||
if (m.per_model_uniform) { wgpuBufferRelease(m.per_model_uniform); m.per_model_uniform = nullptr; }
|
||||
m.vertex_bytes = 0;
|
||||
m.index_count = 0;
|
||||
m.mesh_count = 0;
|
||||
m.instance_count = 0;
|
||||
m.visible_buffer_capacity = 0;
|
||||
m.visible_draws_capacity = 0;
|
||||
m.prefix_sums_capacity = 0;
|
||||
m.total_visible_vertices = 0;
|
||||
m.total_visible_draws = 0;
|
||||
m.meshes.clear();
|
||||
m.instances.clear();
|
||||
m.mesh_draws.clear();
|
||||
m.visible_flat_scratch.clear();
|
||||
m.visible_draws_scratch.clear();
|
||||
m.prefix_sums_scratch.clear();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// WGSL main pipeline — vertex pulling.
|
||||
// WGSL main pipeline — cross-mesh vertex pulling.
|
||||
//
|
||||
// Vertex bytes are read manually from a u32 storage buffer (3 u32 per vertex,
|
||||
// matching INSTANCED_VERTEX_STRIDE_BYTES=12). gl_BaseVertex is folded into
|
||||
// vertex_index by the WebGPU IA, so per-mesh draws set baseVertex =
|
||||
// mesh.vbo_byte_offset / 12 and the shader indexes the global storage array
|
||||
// directly. firstInstance carries the global instance slot, fetched as
|
||||
// @builtin(instance_index).
|
||||
// We issue ONE draw() call per model per frame. The vertex shader binary-
|
||||
// searches the prefix-sum table to find which visible-draw entry the current
|
||||
// @builtin(vertex_index) belongs to, then manually fetches the index and the
|
||||
// 12-byte packed vertex from storage buffers. This avoids the N-drawcalls-per-
|
||||
// frame CPU overhead of per-mesh draws (which dominated on scenes with many
|
||||
// unique meshes — wgpu-native overhead is ~5 µs/draw, so 27k draws = 135ms).
|
||||
//
|
||||
// Binary search cost is O(log N) per vertex, with N up to a few hundred
|
||||
// thousand on dense scenes. Adjacent vertices in the same draw entry share
|
||||
// the search result inside a warp, so memory-coherence keeps this cheap on
|
||||
// GPU.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
static const char* MAIN_WGSL = R"(
|
||||
@@ -180,15 +192,29 @@ struct FrameUniforms {
|
||||
ground_color: vec4<f32>,
|
||||
};
|
||||
|
||||
struct VisibleDraw {
|
||||
mesh_id: u32,
|
||||
instance_idx: u32,
|
||||
ebo_first_u32: u32,
|
||||
base_vertex: u32,
|
||||
};
|
||||
|
||||
struct PerModel {
|
||||
draw_count: u32,
|
||||
total_vertex_count: u32,
|
||||
_pad0: u32,
|
||||
_pad1: u32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> u_frame: FrameUniforms;
|
||||
|
||||
@group(1) @binding(0) var<storage, read> vertices: array<u32>;
|
||||
@group(1) @binding(1) var<storage, read> meshes: array<MeshQuant>;
|
||||
@group(1) @binding(2) var<storage, read> instances: array<InstanceRecord>;
|
||||
// Per-frame compacted visible list: visible[firstInstance + i] picks the
|
||||
// real instance for this draw slot. firstInstance is set per drawIndexed
|
||||
// call so each mesh reads its own contiguous slice of `visible`.
|
||||
@group(1) @binding(3) var<storage, read> visible: array<u32>;
|
||||
@group(1) @binding(0) var<storage, read> vertices: array<u32>;
|
||||
@group(1) @binding(1) var<storage, read> meshes: array<MeshQuant>;
|
||||
@group(1) @binding(2) var<storage, read> instances: array<InstanceRecord>;
|
||||
@group(1) @binding(3) var<storage, read> indices: array<u32>;
|
||||
@group(1) @binding(4) var<storage, read> visible_draws: array<VisibleDraw>;
|
||||
@group(1) @binding(5) var<storage, read> prefix_sums: array<u32>;
|
||||
@group(1) @binding(6) var<uniform> u_model: PerModel;
|
||||
|
||||
struct VsOut {
|
||||
@builtin(position) clip_pos: vec4<f32>,
|
||||
@@ -214,16 +240,46 @@ fn octDecode(e: vec2<f32>) -> vec3<f32> {
|
||||
return normalize(n);
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vid: u32,
|
||||
@builtin(instance_index) iid: u32) -> VsOut {
|
||||
let inst_idx = visible[iid];
|
||||
let inst = instances[inst_idx];
|
||||
let mq = meshes[inst.mesh_id];
|
||||
// Binary search for the largest i in [0, draw_count) with prefix_sums[i] <= vid.
|
||||
// prefix_sums is monotonic non-decreasing and contains draw_count+1 entries
|
||||
// (prefix_sums[draw_count] == total_vertex_count).
|
||||
fn find_draw(vid: u32) -> u32 {
|
||||
var lo: u32 = 0u;
|
||||
var hi: u32 = u_model.draw_count;
|
||||
while (lo + 1u < hi) {
|
||||
let mid = (lo + hi) >> 1u;
|
||||
if (prefix_sums[mid] <= vid) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
return lo;
|
||||
}
|
||||
|
||||
let w0 = vertices[vid * 3u + 0u];
|
||||
let w1 = vertices[vid * 3u + 1u];
|
||||
let w2 = vertices[vid * 3u + 2u];
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vid: u32) -> VsOut {
|
||||
// Saturate past the end (shouldn't happen given draw() count, but safe).
|
||||
if (vid >= u_model.total_vertex_count) {
|
||||
var degen: VsOut;
|
||||
degen.clip_pos = vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
||||
return degen;
|
||||
}
|
||||
|
||||
let draw_idx = find_draw(vid);
|
||||
let local_v = vid - prefix_sums[draw_idx];
|
||||
let item = visible_draws[draw_idx];
|
||||
|
||||
// Fetch the mesh-local index then the global vertex index.
|
||||
let mesh_local_index = indices[item.ebo_first_u32 + local_v];
|
||||
let v_global = item.base_vertex + mesh_local_index;
|
||||
|
||||
let inst = instances[item.instance_idx];
|
||||
let mq = meshes[item.mesh_id];
|
||||
|
||||
let w0 = vertices[v_global * 3u + 0u];
|
||||
let w1 = vertices[v_global * 3u + 1u];
|
||||
let w2 = vertices[v_global * 3u + 2u];
|
||||
|
||||
let px = f32(w0 & 0xFFFFu) / 65535.0;
|
||||
let py = f32((w0 >> 16u) & 0xFFFFu) / 65535.0;
|
||||
@@ -428,11 +484,13 @@ void WgpuViewportWindow::applyCachedModel(uint32_t model_id, SidecarData data) {
|
||||
WGPUBufferUsage_Storage,
|
||||
"model.vertex_storage");
|
||||
|
||||
// Index buffer — mesh-local u32 indices; baseVertex applied per-draw.
|
||||
// Index buffer — mesh-local u32 indices. Bound as a read-only storage
|
||||
// buffer in the vertex shader (cross-mesh vertex pulling manually fetches
|
||||
// indices); Index usage is kept for forward compatibility but never used.
|
||||
m.index_buffer = createBufferWithData(
|
||||
device_, queue_,
|
||||
data.indices.data(), data.indices.size() * sizeof(uint32_t),
|
||||
WGPUBufferUsage_Index,
|
||||
WGPUBufferUsage_Storage | WGPUBufferUsage_Index,
|
||||
"model.index_buffer");
|
||||
|
||||
// Derive MeshGpu[] (vec4 aabb_min + vec4 aabb_max) from MeshInfo's
|
||||
@@ -476,24 +534,43 @@ void WgpuViewportWindow::applyCachedModel(uint32_t model_id, SidecarData data) {
|
||||
WGPUBufferUsage_Storage,
|
||||
"model.instance_storage");
|
||||
|
||||
// Pre-size the visible buffer for the worst case (every instance visible).
|
||||
// Per-frame cull writes a u32 list into it via wgpuQueueWriteBuffer; we
|
||||
// never grow it so the bind group reference stays valid for the model's
|
||||
// lifetime. Minimum size 4 bytes — wgpu rejects zero-sized buffers.
|
||||
const size_t visible_bytes = std::max<size_t>(
|
||||
size_t(data.instances.size()) * sizeof(uint32_t), 4u);
|
||||
WGPUBufferDescriptor vb_desc = {};
|
||||
vb_desc.size = visible_bytes;
|
||||
vb_desc.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst;
|
||||
vb_desc.label = svFromCStr("model.visible_buffer");
|
||||
m.visible_buffer = wgpuDeviceCreateBuffer(device_, &vb_desc);
|
||||
m.visible_buffer_capacity = std::max<size_t>(data.instances.size(), 1u);
|
||||
// Per-frame buffers for cross-mesh vertex pulling. Sized for the worst
|
||||
// case so cullModelCpu only needs wgpuQueueWriteBuffer (never recreate)
|
||||
// — keeps the bind group reference valid for the model's lifetime.
|
||||
//
|
||||
// VisibleDraw capacity: up to one entry per (instance × 2 LODs). LOD1 is
|
||||
// only used when bake produced one, but allocating headroom is cheap
|
||||
// (~3 MB worst-case for a 100k-instance model).
|
||||
const size_t draw_cap = std::max<size_t>(size_t(data.instances.size()) * 2u, 1u);
|
||||
const size_t draws_bytes = draw_cap * sizeof(WgpuModelGpuData::VisibleDrawGpu);
|
||||
WGPUBufferDescriptor vd_desc = {};
|
||||
vd_desc.size = std::max<uint64_t>(draws_bytes, 16);
|
||||
vd_desc.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst;
|
||||
vd_desc.label = svFromCStr("model.visible_draws");
|
||||
m.visible_draws_buffer = wgpuDeviceCreateBuffer(device_, &vd_desc);
|
||||
m.visible_draws_capacity = draw_cap;
|
||||
|
||||
// prefix_sums: draw_cap + 1 entries (final entry = total vertex count).
|
||||
const size_t ps_cap = draw_cap + 1;
|
||||
WGPUBufferDescriptor ps_desc = {};
|
||||
ps_desc.size = std::max<uint64_t>(ps_cap * sizeof(uint32_t), 16);
|
||||
ps_desc.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst;
|
||||
ps_desc.label = svFromCStr("model.prefix_sums");
|
||||
m.prefix_sums_buffer = wgpuDeviceCreateBuffer(device_, &ps_desc);
|
||||
m.prefix_sums_capacity = ps_cap;
|
||||
|
||||
// 16-byte per-model uniform: (draw_count, total_vertex_count, _pad, _pad).
|
||||
WGPUBufferDescriptor mu_desc = {};
|
||||
mu_desc.size = 16;
|
||||
mu_desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
|
||||
mu_desc.label = svFromCStr("model.per_model_uniform");
|
||||
m.per_model_uniform = wgpuDeviceCreateBuffer(device_, &mu_desc);
|
||||
|
||||
// Hand off CPU mirrors (cull / picking will need them later).
|
||||
m.meshes = std::move(data.meshes);
|
||||
m.instances = std::move(data.instances);
|
||||
m.mesh_draws.assign(m.meshes.size(), WgpuModelGpuData::MeshDraw{});
|
||||
m.visible_flat_scratch.reserve(m.instances.size());
|
||||
m.visible_draws_scratch.reserve(draw_cap);
|
||||
m.prefix_sums_scratch.reserve(ps_cap);
|
||||
|
||||
auto [inserted, _] = models_gpu_.emplace(model_id, std::move(m));
|
||||
WgpuModelGpuData& mref = inserted->second;
|
||||
@@ -1005,7 +1082,19 @@ void WgpuViewportWindow::ensureHizTextures(int viewport_w, int viewport_h) {
|
||||
|
||||
if (hiz_resolve_view_) { wgpuTextureViewRelease(hiz_resolve_view_); hiz_resolve_view_ = nullptr; }
|
||||
if (hiz_resolve_texture_) { wgpuTextureRelease(hiz_resolve_texture_); hiz_resolve_texture_ = nullptr; }
|
||||
if (hiz_staging_buffer_) { wgpuBufferRelease(hiz_staging_buffer_); hiz_staging_buffer_ = nullptr; }
|
||||
for (int s = 0; s < HIZ_SLOTS; ++s) {
|
||||
if (hiz_staging_buffers_[s]) {
|
||||
// Force any pending map to finish before release (defensive: shouldn't happen on resize).
|
||||
if (hiz_slot_state_[s] == HizSlotState::Mapped) {
|
||||
wgpuBufferUnmap(hiz_staging_buffers_[s]);
|
||||
}
|
||||
wgpuBufferRelease(hiz_staging_buffers_[s]);
|
||||
hiz_staging_buffers_[s] = nullptr;
|
||||
}
|
||||
hiz_slot_state_[s] = HizSlotState::Idle;
|
||||
}
|
||||
hiz_write_idx_ = 0;
|
||||
hiz_valid_ = false;
|
||||
if (hiz_bind_group_) { wgpuBindGroupRelease(hiz_bind_group_); hiz_bind_group_ = nullptr; }
|
||||
|
||||
WGPUTextureDescriptor desc = {};
|
||||
@@ -1028,15 +1117,19 @@ void WgpuViewportWindow::ensureHizTextures(int viewport_w, int viewport_h) {
|
||||
vdesc.aspect = WGPUTextureAspect_DepthOnly;
|
||||
hiz_resolve_view_ = wgpuTextureCreateView(hiz_resolve_texture_, &vdesc);
|
||||
|
||||
// Staging buffer: pad each row to 256-byte alignment.
|
||||
// Staging buffers: pad each row to 256-byte alignment. Two slots
|
||||
// ping-pong so GPU fill of slot N overlaps CPU read of slot N-1.
|
||||
hiz_padded_bpr_ = uint32_t(
|
||||
(dst_w * sizeof(float) + WGPU_BYTES_PER_ROW_ALIGN - 1)
|
||||
/ WGPU_BYTES_PER_ROW_ALIGN * WGPU_BYTES_PER_ROW_ALIGN);
|
||||
WGPUBufferDescriptor bdesc = {};
|
||||
bdesc.size = uint64_t(hiz_padded_bpr_) * uint64_t(dst_h);
|
||||
bdesc.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead;
|
||||
bdesc.label = svFromCStr("ifcviewer-wgpu.hiz_staging");
|
||||
hiz_staging_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc);
|
||||
for (int s = 0; s < HIZ_SLOTS; ++s) {
|
||||
WGPUBufferDescriptor bdesc = {};
|
||||
bdesc.size = uint64_t(hiz_padded_bpr_) * uint64_t(dst_h);
|
||||
bdesc.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead;
|
||||
bdesc.label = svFromCStr(s == 0 ? "ifcviewer-wgpu.hiz_staging[0]"
|
||||
: "ifcviewer-wgpu.hiz_staging[1]");
|
||||
hiz_staging_buffers_[s] = wgpuDeviceCreateBuffer(device_, &bdesc);
|
||||
}
|
||||
|
||||
hiz_resolve_w_ = dst_w;
|
||||
hiz_resolve_h_ = dst_h;
|
||||
@@ -1048,7 +1141,17 @@ void WgpuViewportWindow::releaseHizResources() {
|
||||
if (hiz_uniform_buffer_) { wgpuBufferRelease(hiz_uniform_buffer_); hiz_uniform_buffer_ = nullptr; }
|
||||
if (hiz_resolve_view_) { wgpuTextureViewRelease(hiz_resolve_view_); hiz_resolve_view_ = nullptr; }
|
||||
if (hiz_resolve_texture_) { wgpuTextureRelease(hiz_resolve_texture_); hiz_resolve_texture_ = nullptr; }
|
||||
if (hiz_staging_buffer_) { wgpuBufferRelease(hiz_staging_buffer_); hiz_staging_buffer_ = nullptr; }
|
||||
for (int s = 0; s < HIZ_SLOTS; ++s) {
|
||||
if (hiz_staging_buffers_[s]) {
|
||||
if (hiz_slot_state_[s] == HizSlotState::Mapped) {
|
||||
wgpuBufferUnmap(hiz_staging_buffers_[s]);
|
||||
}
|
||||
wgpuBufferRelease(hiz_staging_buffers_[s]);
|
||||
hiz_staging_buffers_[s] = nullptr;
|
||||
}
|
||||
hiz_slot_state_[s] = HizSlotState::Idle;
|
||||
}
|
||||
hiz_write_idx_ = 0;
|
||||
if (hiz_pipeline_) { wgpuRenderPipelineRelease(hiz_pipeline_); hiz_pipeline_ = nullptr; }
|
||||
if (hiz_shader_module_) { wgpuShaderModuleRelease(hiz_shader_module_); hiz_shader_module_ = nullptr; }
|
||||
if (hiz_pipeline_layout_) { wgpuPipelineLayoutRelease(hiz_pipeline_layout_); hiz_pipeline_layout_ = nullptr; }
|
||||
@@ -1061,8 +1164,19 @@ void WgpuViewportWindow::releaseHizResources() {
|
||||
hiz_mip_h_.clear();
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::encodeHizResolve(WGPUCommandEncoder enc) {
|
||||
if (!hiz_enabled_ || !hiz_pipeline_ || !hiz_resolve_view_ || !depth_view_) return;
|
||||
int WgpuViewportWindow::encodeHizResolve(WGPUCommandEncoder enc) {
|
||||
if (!hiz_enabled_ || !hiz_pipeline_ || !hiz_resolve_view_ || !depth_view_) return -1;
|
||||
|
||||
// Pick an idle ping-pong slot. If both slots are in flight, skip the
|
||||
// resolve for this frame — the cull keeps using whatever pyramid we
|
||||
// already built (slightly more stale than usual, but never blocks).
|
||||
int slot = -1;
|
||||
for (int s = 0; s < HIZ_SLOTS; ++s) {
|
||||
const int idx = (hiz_write_idx_ + s) % HIZ_SLOTS;
|
||||
if (hiz_slot_state_[idx] == HizSlotState::Idle) { slot = idx; break; }
|
||||
}
|
||||
if (slot < 0) return -1;
|
||||
hiz_write_idx_ = (slot + 1) % HIZ_SLOTS;
|
||||
|
||||
// (Re)build the bind group every frame is wasteful; only rebuild when the
|
||||
// depth view itself was replaced (driven by surface resize). For now we
|
||||
@@ -1110,13 +1224,13 @@ void WgpuViewportWindow::encodeHizResolve(WGPUCommandEncoder enc) {
|
||||
wgpuRenderPassEncoderEnd(pass);
|
||||
wgpuRenderPassEncoderRelease(pass);
|
||||
|
||||
// Now copy the small resolved depth texture into the staging buffer.
|
||||
// Copy the small resolved depth texture into the chosen staging slot.
|
||||
WGPUTexelCopyTextureInfo src = {};
|
||||
src.texture = hiz_resolve_texture_;
|
||||
src.aspect = WGPUTextureAspect_DepthOnly;
|
||||
|
||||
WGPUTexelCopyBufferInfo dst = {};
|
||||
dst.buffer = hiz_staging_buffer_;
|
||||
dst.buffer = hiz_staging_buffers_[slot];
|
||||
dst.layout.bytesPerRow = hiz_padded_bpr_;
|
||||
dst.layout.rowsPerImage = hiz_resolve_h_;
|
||||
|
||||
@@ -1126,89 +1240,112 @@ void WgpuViewportWindow::encodeHizResolve(WGPUCommandEncoder enc) {
|
||||
extent.depthOrArrayLayers = 1;
|
||||
|
||||
wgpuCommandEncoderCopyTextureToBuffer(enc, &src, &dst, &extent);
|
||||
return slot;
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::readbackAndBuildHizPyramid(const QMatrix4x4& vp_used) {
|
||||
if (!hiz_enabled_ || !hiz_staging_buffer_ || hiz_resolve_w_ == 0) return;
|
||||
void WgpuViewportWindow::startHizMap(int slot, const QMatrix4x4& vp_used) {
|
||||
if (slot < 0 || slot >= HIZ_SLOTS) return;
|
||||
if (!hiz_staging_buffers_[slot] || hiz_resolve_w_ == 0) return;
|
||||
|
||||
struct MapReq { bool done = false; bool ok = false; };
|
||||
MapReq req;
|
||||
hiz_slot_vp_[slot] = vp_used;
|
||||
hiz_slot_state_[slot] = HizSlotState::Mapping;
|
||||
|
||||
struct MapCtx { WgpuViewportWindow* self; int slot; };
|
||||
auto* ctx = new MapCtx{ this, slot };
|
||||
|
||||
WGPUBufferMapCallbackInfo mcb = {};
|
||||
mcb.mode = WGPUCallbackMode_AllowProcessEvents;
|
||||
mcb.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*msg*/,
|
||||
void* ud1, void* /*ud2*/) {
|
||||
auto* r = static_cast<MapReq*>(ud1);
|
||||
r->done = true;
|
||||
r->ok = (status == WGPUMapAsyncStatus_Success);
|
||||
auto* c = static_cast<MapCtx*>(ud1);
|
||||
if (status == WGPUMapAsyncStatus_Success) {
|
||||
c->self->hiz_slot_state_[c->slot] = HizSlotState::Mapped;
|
||||
} else {
|
||||
c->self->hiz_slot_state_[c->slot] = HizSlotState::Idle;
|
||||
}
|
||||
delete c;
|
||||
};
|
||||
mcb.userdata1 = &req;
|
||||
mcb.userdata1 = ctx;
|
||||
|
||||
const size_t map_size = size_t(hiz_padded_bpr_) * size_t(hiz_resolve_h_);
|
||||
wgpuBufferMapAsync(hiz_staging_buffer_, WGPUMapMode_Read, 0, map_size, mcb);
|
||||
while (!req.done) wgpuInstanceProcessEvents(instance_);
|
||||
if (!req.ok) return;
|
||||
wgpuBufferMapAsync(hiz_staging_buffers_[slot], WGPUMapMode_Read,
|
||||
0, map_size, mcb);
|
||||
}
|
||||
|
||||
const uint8_t* mapped = static_cast<const uint8_t*>(
|
||||
wgpuBufferGetConstMappedRange(hiz_staging_buffer_, 0, map_size));
|
||||
void WgpuViewportWindow::drainHizReadbacks() {
|
||||
if (!hiz_enabled_ || hiz_resolve_w_ == 0) return;
|
||||
// Process any callbacks that have fired since last frame. Does NOT block:
|
||||
// wgpuInstanceProcessEvents returns immediately after running ready
|
||||
// callbacks. The mapAsync mode is AllowProcessEvents, so this is the
|
||||
// correct drainage point.
|
||||
wgpuInstanceProcessEvents(instance_);
|
||||
|
||||
// Mip 0: tight copy, stripping per-row padding.
|
||||
const uint32_t W0 = hiz_resolve_w_;
|
||||
const uint32_t H0 = hiz_resolve_h_;
|
||||
for (int slot = 0; slot < HIZ_SLOTS; ++slot) {
|
||||
if (hiz_slot_state_[slot] != HizSlotState::Mapped) continue;
|
||||
|
||||
// Pre-size storage: count levels until we hit 1×1.
|
||||
hiz_mip_offset_.clear();
|
||||
hiz_mip_w_.clear();
|
||||
hiz_mip_h_.clear();
|
||||
uint32_t total_texels = 0;
|
||||
{
|
||||
uint32_t w = W0, h = H0;
|
||||
while (true) {
|
||||
hiz_mip_offset_.push_back(total_texels);
|
||||
hiz_mip_w_.push_back(w);
|
||||
hiz_mip_h_.push_back(h);
|
||||
total_texels += w * h;
|
||||
if (w == 1 && h == 1) break;
|
||||
w = std::max(1u, w / 2u);
|
||||
h = std::max(1u, h / 2u);
|
||||
const size_t map_size = size_t(hiz_padded_bpr_) * size_t(hiz_resolve_h_);
|
||||
const uint8_t* mapped = static_cast<const uint8_t*>(
|
||||
wgpuBufferGetConstMappedRange(hiz_staging_buffers_[slot], 0, map_size));
|
||||
|
||||
const uint32_t W0 = hiz_resolve_w_;
|
||||
const uint32_t H0 = hiz_resolve_h_;
|
||||
|
||||
// (Re)build mip pyramid metadata if dimensions changed.
|
||||
if (hiz_mip_offset_.empty()
|
||||
|| hiz_mip_w_.empty() || hiz_mip_w_[0] != W0
|
||||
|| hiz_mip_h_.empty() || hiz_mip_h_[0] != H0) {
|
||||
hiz_mip_offset_.clear();
|
||||
hiz_mip_w_.clear();
|
||||
hiz_mip_h_.clear();
|
||||
uint32_t total = 0;
|
||||
uint32_t w = W0, h = H0;
|
||||
while (true) {
|
||||
hiz_mip_offset_.push_back(total);
|
||||
hiz_mip_w_.push_back(w);
|
||||
hiz_mip_h_.push_back(h);
|
||||
total += w * h;
|
||||
if (w == 1 && h == 1) break;
|
||||
w = std::max(1u, w / 2u);
|
||||
h = std::max(1u, h / 2u);
|
||||
}
|
||||
hiz_pyramid_.assign(total, 0.0f);
|
||||
}
|
||||
}
|
||||
hiz_pyramid_.assign(total_texels, 0.0f);
|
||||
|
||||
// Mip 0: strip row padding.
|
||||
for (uint32_t y = 0; y < H0; ++y) {
|
||||
std::memcpy(&hiz_pyramid_[y * W0],
|
||||
mapped + size_t(y) * hiz_padded_bpr_,
|
||||
W0 * sizeof(float));
|
||||
}
|
||||
wgpuBufferUnmap(hiz_staging_buffer_);
|
||||
// Mip 0: strip per-row padding.
|
||||
for (uint32_t y = 0; y < H0; ++y) {
|
||||
std::memcpy(&hiz_pyramid_[y * W0],
|
||||
mapped + size_t(y) * hiz_padded_bpr_,
|
||||
W0 * sizeof(float));
|
||||
}
|
||||
wgpuBufferUnmap(hiz_staging_buffers_[slot]);
|
||||
hiz_slot_state_[slot] = HizSlotState::Idle;
|
||||
|
||||
// Higher mips: max-reduce 2×2 children. Edge handling: if parent
|
||||
// dimension shrunk to 1, just read the single child.
|
||||
for (size_t L = 1; L < hiz_mip_offset_.size(); ++L) {
|
||||
const uint32_t prev_w = hiz_mip_w_[L - 1];
|
||||
const uint32_t prev_h = hiz_mip_h_[L - 1];
|
||||
const uint32_t this_w = hiz_mip_w_[L];
|
||||
const uint32_t this_h = hiz_mip_h_[L];
|
||||
const float* src = &hiz_pyramid_[hiz_mip_offset_[L - 1]];
|
||||
float* dst = &hiz_pyramid_[hiz_mip_offset_[L]];
|
||||
for (uint32_t y = 0; y < this_h; ++y) {
|
||||
for (uint32_t x = 0; x < this_w; ++x) {
|
||||
const uint32_t x0 = std::min(prev_w - 1, x * 2u);
|
||||
const uint32_t y0 = std::min(prev_h - 1, y * 2u);
|
||||
const uint32_t x1 = std::min(prev_w - 1, x0 + 1u);
|
||||
const uint32_t y1 = std::min(prev_h - 1, y0 + 1u);
|
||||
const float a = src[y0 * prev_w + x0];
|
||||
const float b = src[y0 * prev_w + x1];
|
||||
const float c = src[y1 * prev_w + x0];
|
||||
const float d = src[y1 * prev_w + x1];
|
||||
dst[y * this_w + x] = std::max(std::max(a, b), std::max(c, d));
|
||||
// Higher mips: max-reduce 2×2 children.
|
||||
for (size_t L = 1; L < hiz_mip_offset_.size(); ++L) {
|
||||
const uint32_t prev_w = hiz_mip_w_[L - 1];
|
||||
const uint32_t prev_h = hiz_mip_h_[L - 1];
|
||||
const uint32_t this_w = hiz_mip_w_[L];
|
||||
const uint32_t this_h = hiz_mip_h_[L];
|
||||
const float* src = &hiz_pyramid_[hiz_mip_offset_[L - 1]];
|
||||
float* dst = &hiz_pyramid_[hiz_mip_offset_[L]];
|
||||
for (uint32_t y = 0; y < this_h; ++y) {
|
||||
for (uint32_t x = 0; x < this_w; ++x) {
|
||||
const uint32_t x0 = std::min(prev_w - 1, x * 2u);
|
||||
const uint32_t y0 = std::min(prev_h - 1, y * 2u);
|
||||
const uint32_t x1 = std::min(prev_w - 1, x0 + 1u);
|
||||
const uint32_t y1 = std::min(prev_h - 1, y0 + 1u);
|
||||
const float a = src[y0 * prev_w + x0];
|
||||
const float b = src[y0 * prev_w + x1];
|
||||
const float c = src[y1 * prev_w + x0];
|
||||
const float d = src[y1 * prev_w + x1];
|
||||
dst[y * this_w + x] = std::max(std::max(a, b), std::max(c, d));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hiz_vp_ = vp_used;
|
||||
hiz_valid_ = true;
|
||||
hiz_vp_ = hiz_slot_vp_[slot];
|
||||
hiz_valid_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool WgpuViewportWindow::aabbOccludedByHiz(const float mn[3], const float mx[3]) const {
|
||||
@@ -1302,47 +1439,41 @@ void WgpuViewportWindow::setBenchmarkFrames(int frames) {
|
||||
if (isExposed() && bench_total_ > 0) requestUpdate();
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::cullModelCpu(WgpuModelGpuData& m,
|
||||
const float planes[6][4],
|
||||
const float eye[3], const float forward[3],
|
||||
float focal_px,
|
||||
float min_radius_px,
|
||||
float lod1_threshold_px,
|
||||
bool hiz_enabled) {
|
||||
if (m.instances.empty() || m.meshes.empty() || !m.visible_buffer) {
|
||||
for (auto& d : m.mesh_draws) d.instance_count = 0;
|
||||
return;
|
||||
}
|
||||
uint32_t WgpuViewportWindow::cullModelCpuCompute(WgpuModelGpuData& m,
|
||||
const float planes[6][4],
|
||||
const float eye[3], const float forward[3],
|
||||
float focal_px,
|
||||
float min_radius_px,
|
||||
float lod1_threshold_px,
|
||||
bool hiz_enabled) const {
|
||||
m.total_visible_vertices = 0;
|
||||
m.total_visible_draws = 0;
|
||||
uint32_t hiz_rejects = 0;
|
||||
|
||||
// Per-mesh visible-instance buckets split by LOD. Per-frame scratch held
|
||||
// thread-locally so the cull path doesn't allocate.
|
||||
static thread_local std::vector<std::vector<uint32_t>> per_mesh_lod0;
|
||||
static thread_local std::vector<std::vector<uint32_t>> per_mesh_lod1;
|
||||
if (per_mesh_lod0.size() < m.meshes.size()) per_mesh_lod0.resize(m.meshes.size());
|
||||
if (per_mesh_lod1.size() < m.meshes.size()) per_mesh_lod1.resize(m.meshes.size());
|
||||
for (size_t mi = 0; mi < m.meshes.size(); ++mi) {
|
||||
per_mesh_lod0[mi].clear();
|
||||
per_mesh_lod1[mi].clear();
|
||||
if (m.instances.empty() || m.meshes.empty()
|
||||
|| !m.visible_draws_buffer || !m.prefix_sums_buffer || !m.per_model_uniform) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const bool contrib_enabled = (min_radius_px > 0.0f);
|
||||
const bool lod_enabled = (lod1_threshold_px > 0.0f);
|
||||
|
||||
m.visible_draws_scratch.clear();
|
||||
m.prefix_sums_scratch.clear();
|
||||
m.prefix_sums_scratch.push_back(0); // prefix_sums[0] = 0
|
||||
uint32_t running_vertex_count = 0;
|
||||
|
||||
for (uint32_t i = 0; i < uint32_t(m.instances.size()); ++i) {
|
||||
const auto& inst = m.instances[i];
|
||||
if (inst.mesh_id >= m.meshes.size()) continue;
|
||||
if (!aabbInFrustum(inst.world_aabb_min, inst.world_aabb_max, planes)) continue;
|
||||
if (hiz_enabled
|
||||
&& aabbOccludedByHiz(inst.world_aabb_min, inst.world_aabb_max)) {
|
||||
++hiz_reject_count_;
|
||||
continue;
|
||||
}
|
||||
|
||||
const MeshInfo& mesh = m.meshes[inst.mesh_id];
|
||||
|
||||
// Projected bounding-sphere radius in pixels, shared between the
|
||||
// contribution-cull and LOD-pick decisions. Computed once per
|
||||
// instance; guarded against behind-near-plane and division by zero.
|
||||
// Projected bounding-sphere radius in pixels — shared between the
|
||||
// contribution-cull and LOD-pick decisions. Computed before HiZ so
|
||||
// contribution can short-circuit the per-instance HiZ projection
|
||||
// (which is the bulk of cull cost on dense scenes).
|
||||
float projected_px = std::numeric_limits<float>::infinity();
|
||||
if (contrib_enabled || (lod_enabled && mesh.lod1_index_count > 0)) {
|
||||
const float cx = 0.5f * (inst.world_aabb_min[0] + inst.world_aabb_max[0]);
|
||||
@@ -1360,65 +1491,69 @@ void WgpuViewportWindow::cullModelCpu(WgpuModelGpuData& m,
|
||||
}
|
||||
}
|
||||
|
||||
// Contribution cull: drop instances projected below threshold. Most
|
||||
// of the per-frame cost on dense scenes — typically rejects 80-90%
|
||||
// of frustum-visible instances on real BIM models.
|
||||
// Contribution cull before HiZ: HiZ is by far the most expensive
|
||||
// per-instance test (8-corner projection + mip pyramid sample), so
|
||||
// letting cheap contribution drops happen first cuts the HiZ-tested
|
||||
// population by ~5× on real scenes.
|
||||
if (contrib_enabled && projected_px < min_radius_px) continue;
|
||||
|
||||
// LOD pick: switch to LOD1 if the mesh has one baked and we're below
|
||||
// the LOD threshold.
|
||||
if (hiz_enabled
|
||||
&& aabbOccludedByHiz(inst.world_aabb_min, inst.world_aabb_max)) {
|
||||
++hiz_rejects;
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool use_lod1 = lod_enabled
|
||||
&& mesh.lod1_index_count > 0
|
||||
&& projected_px < lod1_threshold_px;
|
||||
|
||||
if (use_lod1) per_mesh_lod1[inst.mesh_id].push_back(i);
|
||||
else per_mesh_lod0[inst.mesh_id].push_back(i);
|
||||
// Emit one VisibleDraw entry per visible instance. Cross-mesh vertex
|
||||
// pulling means the shader binary-searches prefix_sums to locate
|
||||
// this entry from a global vertex_index.
|
||||
WgpuModelGpuData::VisibleDrawGpu d;
|
||||
d.mesh_id = inst.mesh_id;
|
||||
d.instance_idx = i;
|
||||
d.ebo_first_u32 = (use_lod1 ? mesh.lod1_ebo_byte_offset : mesh.ebo_byte_offset)
|
||||
/ uint32_t(sizeof(uint32_t));
|
||||
d.base_vertex = mesh.vbo_byte_offset / INSTANCED_VERTEX_STRIDE_BYTES;
|
||||
m.visible_draws_scratch.push_back(d);
|
||||
|
||||
const uint32_t entry_vert_count = use_lod1 ? mesh.lod1_index_count
|
||||
: mesh.index_count;
|
||||
running_vertex_count += entry_vert_count;
|
||||
m.prefix_sums_scratch.push_back(running_vertex_count);
|
||||
}
|
||||
|
||||
// Flatten into m.visible_flat_scratch as [mesh0 lod0 | mesh0 lod1 | mesh1
|
||||
// lod0 | mesh1 lod1 | …] and emit one MeshDraw per non-empty bucket.
|
||||
m.visible_flat_scratch.clear();
|
||||
m.mesh_draws.clear();
|
||||
m.mesh_draws.reserve(m.meshes.size() * 2);
|
||||
for (uint32_t mi = 0; mi < m.meshes.size(); ++mi) {
|
||||
const MeshInfo& mesh = m.meshes[mi];
|
||||
m.total_visible_draws = uint32_t(m.visible_draws_scratch.size());
|
||||
m.total_visible_vertices = running_vertex_count;
|
||||
return hiz_rejects;
|
||||
}
|
||||
|
||||
if (!per_mesh_lod0[mi].empty()) {
|
||||
WgpuModelGpuData::MeshDraw d;
|
||||
d.first_instance = uint32_t(m.visible_flat_scratch.size());
|
||||
d.instance_count = uint32_t(per_mesh_lod0[mi].size());
|
||||
d.first_index = mesh.ebo_byte_offset / uint32_t(sizeof(uint32_t));
|
||||
d.base_vertex = int32_t(mesh.vbo_byte_offset / INSTANCED_VERTEX_STRIDE_BYTES);
|
||||
d.index_count = mesh.index_count;
|
||||
for (uint32_t inst_idx : per_mesh_lod0[mi]) {
|
||||
m.visible_flat_scratch.push_back(inst_idx);
|
||||
}
|
||||
m.mesh_draws.push_back(d);
|
||||
}
|
||||
if (!per_mesh_lod1[mi].empty()) {
|
||||
WgpuModelGpuData::MeshDraw d;
|
||||
d.first_instance = uint32_t(m.visible_flat_scratch.size());
|
||||
d.instance_count = uint32_t(per_mesh_lod1[mi].size());
|
||||
d.first_index = mesh.lod1_ebo_byte_offset / uint32_t(sizeof(uint32_t));
|
||||
d.base_vertex = int32_t(mesh.vbo_byte_offset / INSTANCED_VERTEX_STRIDE_BYTES);
|
||||
d.index_count = mesh.lod1_index_count;
|
||||
for (uint32_t inst_idx : per_mesh_lod1[mi]) {
|
||||
m.visible_flat_scratch.push_back(inst_idx);
|
||||
}
|
||||
m.mesh_draws.push_back(d);
|
||||
}
|
||||
void WgpuViewportWindow::cullModelCpuUpload(WgpuModelGpuData& m) {
|
||||
if (!m.visible_draws_buffer || !m.prefix_sums_buffer || !m.per_model_uniform) return;
|
||||
|
||||
if (m.total_visible_draws == 0) {
|
||||
// No visible geometry this frame; render() will skip the draw.
|
||||
// Still update the uniform so the shader sees zero (defensive).
|
||||
const uint32_t um[4] = { 0, 0, 0, 0 };
|
||||
wgpuQueueWriteBuffer(queue_, m.per_model_uniform, 0, um, sizeof(um));
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload the visible list. Round up to 4-byte multiple (always true for
|
||||
// u32 arrays). Empty visible list still uploads one zero so the bind
|
||||
// group has something well-defined; the per-mesh loop will skip the draw.
|
||||
const size_t bytes_to_upload = std::max<size_t>(
|
||||
m.visible_flat_scratch.size() * sizeof(uint32_t), sizeof(uint32_t));
|
||||
static const uint32_t zero = 0;
|
||||
const void* src = m.visible_flat_scratch.empty()
|
||||
? static_cast<const void*>(&zero)
|
||||
: static_cast<const void*>(m.visible_flat_scratch.data());
|
||||
wgpuQueueWriteBuffer(queue_, m.visible_buffer, 0, src, bytes_to_upload);
|
||||
wgpuQueueWriteBuffer(queue_, m.visible_draws_buffer, 0,
|
||||
m.visible_draws_scratch.data(),
|
||||
m.visible_draws_scratch.size()
|
||||
* sizeof(WgpuModelGpuData::VisibleDrawGpu));
|
||||
wgpuQueueWriteBuffer(queue_, m.prefix_sums_buffer, 0,
|
||||
m.prefix_sums_scratch.data(),
|
||||
m.prefix_sums_scratch.size() * sizeof(uint32_t));
|
||||
|
||||
const uint32_t um[4] = {
|
||||
m.total_visible_draws,
|
||||
m.total_visible_vertices,
|
||||
0, 0,
|
||||
};
|
||||
wgpuQueueWriteBuffer(queue_, m.per_model_uniform, 0, um, sizeof(um));
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::render() {
|
||||
@@ -1427,6 +1562,10 @@ void WgpuViewportWindow::render() {
|
||||
QElapsedTimer frame_timer;
|
||||
if (bench_total_ > 0) frame_timer.start();
|
||||
|
||||
// Drain any HiZ async readbacks that completed since last frame so the
|
||||
// pyramid is as fresh as it can be before cull runs.
|
||||
if (hiz_enabled_) drainHizReadbacks();
|
||||
|
||||
WGPUSurfaceTexture surf_tex = {};
|
||||
wgpuSurfaceGetCurrentTexture(surface_, &surf_tex);
|
||||
|
||||
@@ -1462,6 +1601,8 @@ void WgpuViewportWindow::render() {
|
||||
last_visible_triangles_ = 0;
|
||||
last_sub_draws_ = 0;
|
||||
hiz_reject_count_ = 0;
|
||||
QElapsedTimer cull_timer;
|
||||
if (bench_total_ > 0) cull_timer.start();
|
||||
QMatrix4x4 vp_this_frame;
|
||||
{
|
||||
const QVector3D target(camera_target_[0], camera_target_[1], camera_target_[2]);
|
||||
@@ -1488,18 +1629,62 @@ void WgpuViewportWindow::render() {
|
||||
/ std::tan(qDegreesToRadians(camera_fov_y_deg_) * 0.5f))
|
||||
: 0.0f;
|
||||
|
||||
// Motion detection: any change in camera state since last frame
|
||||
// bumps the contribution threshold to motion_min_pixel_radius_
|
||||
// (mirrors GL's NavPreset behaviour, drops more sub-pixel work
|
||||
// during orbit/pan/zoom).
|
||||
const bool camera_moved = has_prev_camera_
|
||||
&& (camera_target_[0] != prev_camera_target_[0]
|
||||
|| camera_target_[1] != prev_camera_target_[1]
|
||||
|| camera_target_[2] != prev_camera_target_[2]
|
||||
|| camera_distance_ != prev_camera_distance_
|
||||
|| camera_yaw_deg_ != prev_camera_yaw_deg_
|
||||
|| camera_pitch_deg_ != prev_camera_pitch_deg_);
|
||||
const float effective_min_px =
|
||||
(camera_moved && motion_min_pixel_radius_ > min_pixel_radius_)
|
||||
? motion_min_pixel_radius_
|
||||
: min_pixel_radius_;
|
||||
|
||||
// Cull each model on its own worker thread. wgpu queue writes are
|
||||
// serialised on the main thread after the parallel compute joins —
|
||||
// wgpu-native doesn't guarantee thread-safety on queue ops.
|
||||
std::vector<std::pair<uint32_t, std::future<uint32_t>>> futures;
|
||||
futures.reserve(models_gpu_.size());
|
||||
for (auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden) continue;
|
||||
cullModelCpu(m, planes, eye_a, fwd_a, focal_px,
|
||||
min_pixel_radius_, lod1_pixel_threshold_,
|
||||
hiz_enabled_);
|
||||
for (const auto& d : m.mesh_draws) {
|
||||
if (d.instance_count == 0 || d.index_count == 0) continue;
|
||||
last_visible_objects_ += d.instance_count;
|
||||
last_visible_triangles_ += (d.index_count / 3u) * d.instance_count;
|
||||
last_sub_draws_ += 1;
|
||||
}
|
||||
auto& m_ref = m;
|
||||
futures.emplace_back(mid, std::async(std::launch::async,
|
||||
[this, &m_ref, &planes, &eye_a, &fwd_a,
|
||||
focal_px, effective_min_px]() {
|
||||
return cullModelCpuCompute(
|
||||
m_ref, planes, eye_a, fwd_a, focal_px,
|
||||
effective_min_px, lod1_pixel_threshold_,
|
||||
hiz_enabled_);
|
||||
}));
|
||||
}
|
||||
for (auto& [mid, fut] : futures) {
|
||||
hiz_reject_count_ += fut.get();
|
||||
}
|
||||
for (auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden) continue;
|
||||
cullModelCpuUpload(m);
|
||||
last_visible_objects_ += m.total_visible_draws;
|
||||
last_visible_triangles_ += m.total_visible_vertices / 3u;
|
||||
// One CPU drawcall per model now (cross-mesh vertex pulling).
|
||||
if (m.total_visible_draws > 0) last_sub_draws_ += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot camera state for next frame's motion detection.
|
||||
prev_camera_target_[0] = camera_target_[0];
|
||||
prev_camera_target_[1] = camera_target_[1];
|
||||
prev_camera_target_[2] = camera_target_[2];
|
||||
prev_camera_distance_ = camera_distance_;
|
||||
prev_camera_yaw_deg_ = camera_yaw_deg_;
|
||||
prev_camera_pitch_deg_ = camera_pitch_deg_;
|
||||
has_prev_camera_ = true;
|
||||
if (bench_total_ > 0 && bench_count_ >= bench_warmup_) {
|
||||
bench_cull_ms_total_ += double(cull_timer.nsecsElapsed()) / 1e6;
|
||||
}
|
||||
|
||||
WGPUCommandEncoder enc = wgpuDeviceCreateCommandEncoder(device_, nullptr);
|
||||
@@ -1539,37 +1724,22 @@ void WgpuViewportWindow::render() {
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, frame_bind_group_, 0, nullptr);
|
||||
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden || !m.bind_group || !m.index_buffer || m.index_count == 0) continue;
|
||||
|
||||
if (m.hidden || !m.bind_group || m.total_visible_vertices == 0) continue;
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 1, m.bind_group, 0, nullptr);
|
||||
wgpuRenderPassEncoderSetIndexBuffer(pass, m.index_buffer,
|
||||
WGPUIndexFormat_Uint32,
|
||||
0, WGPU_WHOLE_SIZE);
|
||||
|
||||
// One drawIndexed per non-empty mesh. instanceCount is the number
|
||||
// of frustum-surviving instances of this mesh; firstInstance is
|
||||
// their offset into m.visible_buffer (consumed by the WGSL
|
||||
// `visible[iid]` indirection). All-instances-culled meshes are
|
||||
// skipped — no draw call issued at all.
|
||||
for (const auto& d : m.mesh_draws) {
|
||||
if (d.instance_count == 0 || d.index_count == 0) continue;
|
||||
wgpuRenderPassEncoderDrawIndexed(
|
||||
pass,
|
||||
d.index_count,
|
||||
d.instance_count,
|
||||
d.first_index,
|
||||
d.base_vertex,
|
||||
d.first_instance);
|
||||
}
|
||||
// Single CPU drawcall per model. The vertex shader binary-
|
||||
// searches prefix_sums by vertex_index to find (mesh, instance)
|
||||
// and pulls index + vertex data manually from storage buffers.
|
||||
wgpuRenderPassEncoderDraw(pass, m.total_visible_vertices, 1, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
wgpuRenderPassEncoderEnd(pass);
|
||||
wgpuRenderPassEncoderRelease(pass);
|
||||
|
||||
// ---- HiZ: resolve MSAA depth → small single-sample → staging buffer
|
||||
// ---- HiZ: resolve MSAA depth → small single-sample → ping-pong slot
|
||||
int hiz_submitted_slot = -1;
|
||||
if (hiz_enabled_) {
|
||||
encodeHizResolve(enc);
|
||||
hiz_submitted_slot = encodeHizResolve(enc);
|
||||
}
|
||||
|
||||
// ---- Optional capture: encode copy on the same command buffer -------
|
||||
@@ -1683,11 +1853,17 @@ void WgpuViewportWindow::render() {
|
||||
wgpuSurfacePresent(surface_);
|
||||
wgpuTextureRelease(surf_tex.texture);
|
||||
|
||||
// ---- HiZ readback + mip pyramid for the *next* frame's cull ---------
|
||||
// Sync wait via processEvents — the staging buffer is small (≈ 256×160×4)
|
||||
// so the stall is well under a millisecond on every backend.
|
||||
if (hiz_enabled_) {
|
||||
readbackAndBuildHizPyramid(vp_this_frame);
|
||||
// ---- HiZ async readback handoff -------------------------------------
|
||||
// Don't block — just kick off the mapAsync for the slot we filled this
|
||||
// frame. Drainage happens at the top of the *next* frame via
|
||||
// drainHizReadbacks(), giving the GPU at least one frame of headroom.
|
||||
if (hiz_enabled_ && hiz_submitted_slot >= 0) {
|
||||
QElapsedTimer hiz_timer;
|
||||
if (bench_total_ > 0) hiz_timer.start();
|
||||
startHizMap(hiz_submitted_slot, vp_this_frame);
|
||||
if (bench_total_ > 0 && bench_count_ >= bench_warmup_) {
|
||||
bench_hiz_readback_ms_total_ += double(hiz_timer.nsecsElapsed()) / 1e6;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Benchmark integration + auto-quit -------------------------------
|
||||
@@ -1736,6 +1912,11 @@ void WgpuViewportWindow::render() {
|
||||
<< " tri " << last_visible_triangles_
|
||||
<< " sub_draws " << last_sub_draws_
|
||||
<< " hiz_rej " << hiz_reject_count_;
|
||||
const double n = double(std::max(1, bench_total_));
|
||||
qInfo().noquote().nospace()
|
||||
<< " per-frame avg ms: cull=" << bench_cull_ms_total_ / n
|
||||
<< " hiz_readback=" << bench_hiz_readback_ms_total_ / n
|
||||
<< " hiz=" << (hiz_enabled_ ? "on" : "off");
|
||||
qInfo().noquote() << "=== END BENCHMARK ===\n";
|
||||
|
||||
bench_total_ = 0;
|
||||
@@ -1764,14 +1945,22 @@ bool WgpuViewportWindow::buildPipelines() {
|
||||
frame_bgl_desc.label = svFromCStr("ifcviewer-wgpu.frame_bgl");
|
||||
frame_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &frame_bgl_desc);
|
||||
|
||||
WGPUBindGroupLayoutEntry model_entries[4] = {};
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
// 6 read-only storage buffers (vertices, meshes, instances, indices,
|
||||
// visible_draws, prefix_sums) + 1 uniform (per-model count). All read
|
||||
// in the vertex shader. WebGPU's mandatory min is 8 storage / 12 uniform
|
||||
// per stage, so we're comfortably under the cap.
|
||||
WGPUBindGroupLayoutEntry model_entries[7] = {};
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
model_entries[i].binding = uint32_t(i);
|
||||
model_entries[i].visibility = WGPUShaderStage_Vertex;
|
||||
model_entries[i].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
|
||||
}
|
||||
model_entries[6].binding = 6;
|
||||
model_entries[6].visibility = WGPUShaderStage_Vertex;
|
||||
model_entries[6].buffer.type = WGPUBufferBindingType_Uniform;
|
||||
model_entries[6].buffer.minBindingSize = 16;
|
||||
WGPUBindGroupLayoutDescriptor model_bgl_desc = {};
|
||||
model_bgl_desc.entryCount = 4;
|
||||
model_bgl_desc.entryCount = 7;
|
||||
model_bgl_desc.entries = model_entries;
|
||||
model_bgl_desc.label = svFromCStr("ifcviewer-wgpu.model_bgl");
|
||||
model_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &model_bgl_desc);
|
||||
@@ -1859,12 +2048,14 @@ void WgpuViewportWindow::buildModelBindGroup(WgpuModelGpuData& m) {
|
||||
wgpuBindGroupRelease(m.bind_group);
|
||||
m.bind_group = nullptr;
|
||||
}
|
||||
if (!m.vertex_storage || !m.mesh_storage || !m.instance_storage || !m.visible_buffer) {
|
||||
if (!m.vertex_storage || !m.mesh_storage || !m.instance_storage
|
||||
|| !m.index_buffer || !m.visible_draws_buffer || !m.prefix_sums_buffer
|
||||
|| !m.per_model_uniform) {
|
||||
// Empty model — no bind group needed; the draw loop will skip it.
|
||||
return;
|
||||
}
|
||||
|
||||
WGPUBindGroupEntry entries[4] = {};
|
||||
WGPUBindGroupEntry entries[7] = {};
|
||||
entries[0].binding = 0;
|
||||
entries[0].buffer = m.vertex_storage;
|
||||
entries[0].size = WGPU_WHOLE_SIZE;
|
||||
@@ -1875,12 +2066,21 @@ void WgpuViewportWindow::buildModelBindGroup(WgpuModelGpuData& m) {
|
||||
entries[2].buffer = m.instance_storage;
|
||||
entries[2].size = WGPU_WHOLE_SIZE;
|
||||
entries[3].binding = 3;
|
||||
entries[3].buffer = m.visible_buffer;
|
||||
entries[3].buffer = m.index_buffer;
|
||||
entries[3].size = WGPU_WHOLE_SIZE;
|
||||
entries[4].binding = 4;
|
||||
entries[4].buffer = m.visible_draws_buffer;
|
||||
entries[4].size = WGPU_WHOLE_SIZE;
|
||||
entries[5].binding = 5;
|
||||
entries[5].buffer = m.prefix_sums_buffer;
|
||||
entries[5].size = WGPU_WHOLE_SIZE;
|
||||
entries[6].binding = 6;
|
||||
entries[6].buffer = m.per_model_uniform;
|
||||
entries[6].size = 16;
|
||||
|
||||
WGPUBindGroupDescriptor desc = {};
|
||||
desc.layout = model_bgl_;
|
||||
desc.entryCount = 4;
|
||||
desc.entryCount = 7;
|
||||
desc.entries = entries;
|
||||
desc.label = svFromCStr("ifcviewer-wgpu.model_bind_group");
|
||||
m.bind_group = wgpuDeviceCreateBindGroup(device_, &desc);
|
||||
|
||||
@@ -124,13 +124,19 @@ private:
|
||||
void ensureHizTextures(int viewport_w, int viewport_h);
|
||||
void releaseHizResources();
|
||||
// Resolves the just-rendered MSAA depth into the small single-sample
|
||||
// HiZ texture and copies it to the staging buffer. Encoded onto `enc`
|
||||
// so it ships in the same command buffer as the main draw.
|
||||
void encodeHizResolve(WGPUCommandEncoder enc);
|
||||
// Maps the staging buffer (waits via processEvents), max-reduces a CPU
|
||||
// mip pyramid, stores the VP used. Run after submitting the encoder so
|
||||
// the GPU has begun the copy. Updates hiz_valid_ to true on success.
|
||||
void readbackAndBuildHizPyramid(const QMatrix4x4& vp_used);
|
||||
// HiZ texture and copies it to whichever staging slot is currently
|
||||
// idle. Returns the slot index used, or -1 if both slots are still
|
||||
// in flight (resolve is skipped this frame — fine, we already have
|
||||
// a recent pyramid). Encoded onto `enc` so it ships in the same
|
||||
// command buffer as the main draw.
|
||||
int encodeHizResolve(WGPUCommandEncoder enc);
|
||||
// Issues a non-blocking mapAsync on `slot` after submit, so the
|
||||
// callback can fire whenever the GPU has actually finished writing.
|
||||
void startHizMap(int slot, const QMatrix4x4& vp_used);
|
||||
// Drains pending mapAsync callbacks (via processEvents — does NOT
|
||||
// block on GPU work). For any slot that just signalled Mapped, reads
|
||||
// it, unmaps it, max-reduces the mip pyramid, and updates hiz_vp_.
|
||||
void drainHizReadbacks();
|
||||
// Project AABB through hiz_vp_ and test against the pyramid. False
|
||||
// (keep) if HiZ isn't valid yet, AABB straddles the near plane, or
|
||||
// any projection is unreliable. True (cull) when AABB is provably
|
||||
@@ -156,13 +162,22 @@ private:
|
||||
// `lod1_threshold_px` — survivors projected below this get the mesh's
|
||||
// LOD1 index slice when one was baked.
|
||||
// min_radius_px == 0 disables contribution culling.
|
||||
void cullModelCpu(WgpuModelGpuData& m,
|
||||
const float planes[6][4],
|
||||
const float eye[3], const float forward[3],
|
||||
float focal_px,
|
||||
float min_radius_px,
|
||||
float lod1_threshold_px,
|
||||
bool hiz_enabled);
|
||||
// CPU-only phase of cull: produces m.visible_draws_scratch /
|
||||
// prefix_sums_scratch and sets total_visible_draws / total_visible_
|
||||
// vertices. Touches no wgpu state, so this can run on a worker thread
|
||||
// (multiple models culled in parallel). Returns the number of HiZ
|
||||
// rejections accumulated (caller adds to the per-frame stat).
|
||||
uint32_t cullModelCpuCompute(WgpuModelGpuData& m,
|
||||
const float planes[6][4],
|
||||
const float eye[3], const float forward[3],
|
||||
float focal_px,
|
||||
float min_radius_px,
|
||||
float lod1_threshold_px,
|
||||
bool hiz_enabled) const;
|
||||
// Upload phase: wgpuQueueWriteBuffer for visible_draws / prefix_sums /
|
||||
// per-model uniform. Main-thread only (wgpu queue ops are not all
|
||||
// thread-safe).
|
||||
void cullModelCpuUpload(WgpuModelGpuData& m);
|
||||
|
||||
bool wgpu_initialized_ = false;
|
||||
bool surface_configured_ = false;
|
||||
@@ -221,11 +236,25 @@ private:
|
||||
|
||||
WGPUTexture hiz_resolve_texture_ = nullptr;
|
||||
WGPUTextureView hiz_resolve_view_ = nullptr;
|
||||
WGPUBuffer hiz_staging_buffer_ = nullptr;
|
||||
uint32_t hiz_resolve_w_ = 0;
|
||||
uint32_t hiz_resolve_h_ = 0;
|
||||
uint32_t hiz_padded_bpr_ = 0; // bytes per row in the staging buffer
|
||||
|
||||
// Ping-pong async readback. Frame N submits a copy into slot
|
||||
// hiz_write_idx_ and calls mapAsync (non-blocking) on that slot. Frame
|
||||
// N+K (K ≥ 1) calls processEvents to drain callbacks; whichever slot
|
||||
// signalled completion is mapped, read into hiz_pyramid_, and unmapped
|
||||
// — making the pyramid 1+ frames stale, which is fine ("slightly-stale
|
||||
// depth" pattern the GL backend already documents). Two slots overlap
|
||||
// GPU write with CPU read; we never block on the readback.
|
||||
enum class HizSlotState : uint8_t { Idle, Mapping, Mapped };
|
||||
static constexpr int HIZ_SLOTS = 2;
|
||||
WGPUBuffer hiz_staging_buffers_[HIZ_SLOTS] = { nullptr, nullptr };
|
||||
QMatrix4x4 hiz_slot_vp_ [HIZ_SLOTS];
|
||||
HizSlotState hiz_slot_state_ [HIZ_SLOTS] = { HizSlotState::Idle,
|
||||
HizSlotState::Idle };
|
||||
int hiz_write_idx_ = 0;
|
||||
|
||||
// CPU mip pyramid (max-reduce). hiz_pyramid_[hiz_mip_offset_[L] + y*W + x].
|
||||
std::vector<float> hiz_pyramid_;
|
||||
std::vector<uint32_t> hiz_mip_offset_;
|
||||
@@ -247,16 +276,20 @@ private:
|
||||
float camera_near_ = 0.1f;
|
||||
float camera_far_ = 10000.0f;
|
||||
|
||||
// Drop instances whose projected bounding-sphere radius is below this
|
||||
// many pixels. Mirrors AppSettings::minPixelRadius() (GL default 2.0;
|
||||
// motion mode uses 10.0 but we don't differentiate yet — that arrives
|
||||
// with mouse-driven motion-state tracking later).
|
||||
float min_pixel_radius_ = 2.0f;
|
||||
// Contribution-cull thresholds. Still-frame uses min_pixel_radius_;
|
||||
// when the camera changed since last frame, the bigger motion threshold
|
||||
// kicks in to drop more sub-pixel detail (and slash per-frame cull cost).
|
||||
// Matches AppSettings::minPixelRadius / motionMinPixelRadius in GL.
|
||||
float min_pixel_radius_ = 2.0f;
|
||||
float motion_min_pixel_radius_ = 10.0f;
|
||||
|
||||
public:
|
||||
// Master switch for HiZ occlusion. Set false to skip the depth resolve
|
||||
// + readback + cull test entirely (matches IFC_NO_HIZ in the GL backend).
|
||||
bool hiz_enabled_ = true;
|
||||
|
||||
private:
|
||||
|
||||
// Switch to LOD1 when an instance's projected bounding-sphere radius
|
||||
// drops below this many pixels. 0 disables (always LOD0). Defaults
|
||||
// mirror AppSettings::lod1PixelThreshold() in the GL backend.
|
||||
@@ -274,6 +307,15 @@ private:
|
||||
// user pointed it.
|
||||
bool initial_view_applied_ = false;
|
||||
|
||||
// Camera state at the previous render() for motion detection. Any
|
||||
// change means we apply the motion contribution threshold this frame
|
||||
// (drops more sub-pixel work mid-orbit; matches GL behaviour).
|
||||
float prev_camera_target_[3] = { 0, 0, 0 };
|
||||
float prev_camera_distance_ = 0.0f;
|
||||
float prev_camera_yaw_deg_ = 0.0f;
|
||||
float prev_camera_pitch_deg_ = 0.0f;
|
||||
bool has_prev_camera_ = false;
|
||||
|
||||
// Pending one-shot screenshot, captured at the end of the next render().
|
||||
QString pending_screenshot_path_;
|
||||
bool pending_screenshot_quit_ = false;
|
||||
@@ -301,6 +343,13 @@ private:
|
||||
uint32_t last_visible_objects_ = 0;
|
||||
uint32_t last_visible_triangles_ = 0;
|
||||
uint32_t last_sub_draws_ = 0;
|
||||
|
||||
// Phase-time accumulators for benchmark mode. Each window measures a
|
||||
// distinct slice of render() so we can attribute frame cost. Totals
|
||||
// across the timed window are divided by bench_total_ on print.
|
||||
double bench_cull_ms_total_ = 0.0;
|
||||
double bench_hiz_readback_ms_total_ = 0.0;
|
||||
double bench_submit_ms_total_ = 0.0;
|
||||
};
|
||||
|
||||
#endif // WGPUVIEWPORTWINDOW_H
|
||||
|
||||
Reference in New Issue
Block a user