ifcviewer: move render() body into ViewportCore (#84-x)

The frame loop — surface acquisition, parallel cull dispatch, streaming
drive, two-pass main render, HiZ resolve, edge pass, screenshot
capture, FrameStats emission, interactive / bench heartbeat, bench
summary + auto-quit — all live in ViewportCore now.
ViewportWindow::render() shrinks to the Qt-only prelude:
isExposed() guard, fpsIntegrate() (fly-mode WASD step), then
core_.render().

The overlay renderer stays Qt-bound (OverlayRenderer.h carries
QString labels). Core reaches it via two new ViewportHost virtuals:
encodeOverlaysInMainPass (section gizmos, highlights, pivot, lines,
points — in-MSAA-pass) and encodeOverlaysPostMain (corner axis,
marquee, labels — on the resolved surface). The QtViewportHost
implementation in ViewportWindow forwards each to overlays_.X().

FrameStats moves to its own Qt-free header (FrameStats.h) with
ViewportWindow::FrameStats re-exported as a using-alias so the
bonsai-side signal binding keeps working. ViewportHost::onFrameStats
replaces the placeholder 4-double signature with the typed POD.
OverlayFrame moves alongside (OverlayFrame.h) so the host overlay
callbacks can carry it without dragging Qt into core.

Bench + frame-stats + cull-tuning state (min_pixel_radius_,
motion_min_pixel_radius_, lod1_pixel_threshold_, cull_threads_enabled_,
prev_camera_*, has_prev_camera_, last_cull_was_motion_, last_visible_*,
last_cull_*ms_, last_stream_ms_, bench_*, interactive_frame_count_,
frame_time_ms_window_/_sum_/_count_/_head_) move to ViewportCore; VW
keeps reference aliases so the env-var prelude, setBenchmarkFrames,
and the various tool keybind setters keep compiling unchanged.

Smoke checks: --screenshot path renders + saves a clean PNG;
--benchmark 10 runs the warm gate, prints the per-frame log + summary,
and exits cleanly via host_->quit().
This commit is contained in:
Dion Moult
2026-06-06 20:27:33 +10:00
parent 8fb0a4b24f
commit ebb9c91bdf
9 changed files with 863 additions and 1057 deletions
+85
View File
@@ -355,6 +355,19 @@ public:
void finalizeScreenshotCapture(WGPUBuffer capture_buffer,
std::uint32_t padded_bpr);
// ---- Render loop (#84-x) ----------------------------------------------
//
// Encode one frame: acquire the swapchain texture, run cull (parallel
// when WGPU_CULL_THREADS!=0), drive streaming residency, encode the
// two-pass main draw, edge pass, HiZ resolve, and (optionally) the
// screenshot capture. Calls host_->encodeOverlaysInMainPass and
// host_->encodeOverlaysPostMain for the overlay layers (still
// Qt-bound), and host_->onFrameStats for the bench / status bar
// listeners. Idempotent re: framebuffer size — reconfigures the
// surface on Outdated/Lost. Returns early when the surface query
// fails so the next frame retries cleanly.
void render();
// ---- Surface configuration (#84-u) ------------------------------------
//
// Configure the swapchain at the given physical size. Picks a present
@@ -790,6 +803,78 @@ private:
std::unordered_map<std::uint32_t, std::unique_ptr<SidecarData>>
pending_direct_loads_;
// ---- Render-loop state (#84-x) ---------------------------------------
// Contribution-cull thresholds. min_pixel_radius_ is the still-frame
// floor; motion_min_pixel_radius_ kicks in during orbit/pan/zoom to
// drop more sub-pixel work. lod1_pixel_threshold_ chooses LOD1 over
// LOD0 when an instance projects below that radius.
float min_pixel_radius_ = 3.0f;
float motion_min_pixel_radius_ = 15.0f;
float lod1_pixel_threshold_ = 30.0f;
// WGPU_CULL_THREADS=0 forces sequential cull (one model after another)
// for parallel-vs-serial benchmarking. Default ON.
bool cull_threads_enabled_ = true;
// Per-frame stats latched by render() for FrameStats emission +
// the interactive heartbeat / bench per-frame line.
std::uint32_t last_visible_objects_ = 0;
std::uint32_t last_visible_triangles_ = 0;
std::uint32_t last_sub_draws_ = 0;
double last_cull_ms_ = 0.0;
double last_cull_compute_ms_ = 0.0;
double last_cull_upload_ms_ = 0.0;
double last_stream_ms_ = 0.0;
// True when the cull just used motion_min_pixel_radius_ — render()
// schedules one more frame so the camera-now-stopped state recomputes
// the cull at the still threshold and previously dropped sub-pixel
// instances pop back in.
bool last_cull_was_motion_ = false;
// Previous frame's camera state for the motion-vs-still decision.
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;
// Rolling-average FPS readout (60-frame window). frame_time_ms_sum_
// tracks the running sum so FrameStats can divide-by-count without
// re-summing.
static constexpr int FRAME_TIME_WINDOW = 60;
double frame_time_ms_window_[FRAME_TIME_WINDOW] = {};
int frame_time_ms_count_ = 0;
int frame_time_ms_head_ = 0;
double frame_time_ms_sum_ = 0.0;
// Benchmark-mode state. Activated by setBenchmarkFrames(n); render()
// orbits the camera at bench_yaw_speed_ deg/frame for bench_total_
// frames after a bench_warmup_ settle period, collects per-frame ms,
// emits a percentile summary, and calls host_->quit().
int bench_total_ = 0;
int bench_count_ = 0;
int bench_warmup_ = 5;
float bench_yaw_start_ = 0.0f;
float bench_yaw_speed_ = 0.5f; // degrees per frame
std::vector<float> bench_frame_ms_;
// Cold-load warmup gate counters. The orbit sweep waits until
// streaming has converged for CONVERGE_FRAMES_REQUIRED consecutive
// frames before starting the sample collection.
int bench_warm_streak_ = 0;
int bench_warm_frames_total_ = 0;
bool bench_warm_done_ = false;
// Per-frame timing accumulators for the bench summary. Each
// accumulator is divided by bench_total_ when the run finishes.
double bench_cull_ms_total_ = 0.0;
double bench_stream_ms_total_ = 0.0;
double bench_hiz_readback_ms_total_ = 0.0;
// Interactive [frame] heartbeat counter. Used to rate-limit the
// stream-health summary + WGPU_STREAM_DEEP_DEBUG dump.
int interactive_frame_count_ = 0;
// Auto-viewAll suppression. Flipped true by the first applyCachedModel
// (so a fresh scene frames itself) or by any explicit setCamera (so a
// user/bonsai-side camera write isn't overridden by the next model