mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 01:41:57 +00:00
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:
@@ -157,6 +157,8 @@ set(IFCVIEWER_CORE_HEADERS
|
||||
LodBuilder.h
|
||||
Stopwatch.h
|
||||
ModelGpuData.h
|
||||
FrameStats.h
|
||||
OverlayFrame.h
|
||||
SectionPlane.h
|
||||
SelectionState.h
|
||||
SidecarCache.h
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 IFCVIEWER_FRAMESTATS_H
|
||||
#define IFCVIEWER_FRAMESTATS_H
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Per-frame statistics emitted from render() so the embedder can
|
||||
// surface them (bonsai's status bar, the web-side dev console, the
|
||||
// benchmark accumulator). Pure POD so it travels through ViewportHost
|
||||
// without dragging Qt along. ViewportWindow::FrameStats re-exports
|
||||
// this so existing bonsai callers still see the familiar
|
||||
// ViewportWindow::FrameStats type.
|
||||
struct FrameStats {
|
||||
float fps;
|
||||
float frame_time_ms;
|
||||
std::uint32_t total_objects;
|
||||
std::uint32_t visible_objects;
|
||||
std::uint32_t total_triangles;
|
||||
std::uint32_t visible_triangles;
|
||||
std::uint32_t unique_meshes;
|
||||
std::uint32_t gl_draw_calls; // wgpu draw-call count; name kept for bonsai parity
|
||||
std::uint32_t indirect_sub_draws; // sub-draws packed into the chunk-indirect lists
|
||||
};
|
||||
|
||||
#endif // IFCVIEWER_FRAMESTATS_H
|
||||
@@ -0,0 +1,41 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 IFCVIEWER_OVERLAYFRAME_H
|
||||
#define IFCVIEWER_OVERLAYFRAME_H
|
||||
|
||||
#include <Eigen/Dense>
|
||||
|
||||
// Per-frame snapshot of viewport state that every overlay needs. Built
|
||||
// once at the top of render() and passed by const-ref to each encodeX()
|
||||
// call so the overlay renderer never reaches back into the viewport.
|
||||
// Qt-free so ViewportHost can carry it as a callback param.
|
||||
struct OverlayFrame {
|
||||
Eigen::Matrix4f view_proj = Eigen::Matrix4f::Identity();
|
||||
Eigen::Vector3f camera_target = Eigen::Vector3f::Zero();
|
||||
float camera_distance = 5.0f;
|
||||
float camera_yaw_deg = 0.0f;
|
||||
float camera_pitch_deg = 0.0f;
|
||||
float camera_fov_y_deg = 45.0f;
|
||||
int viewport_w_px = 0;
|
||||
int viewport_h_px = 0;
|
||||
int device_pixel_ratio = 1;
|
||||
};
|
||||
|
||||
#endif // IFCVIEWER_OVERLAYFRAME_H
|
||||
@@ -30,21 +30,7 @@
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
// Per-frame snapshot of viewport state that every overlay needs. Built once
|
||||
// at the top of render() and passed by const-ref to each encodeX() call so
|
||||
// the overlay renderer never reaches back into the viewport.
|
||||
struct OverlayFrame {
|
||||
Eigen::Matrix4f view_proj = Eigen::Matrix4f::Identity();
|
||||
Eigen::Vector3f camera_target = Eigen::Vector3f::Zero();
|
||||
float camera_distance = 5.0f;
|
||||
float camera_yaw_deg = 0.0f;
|
||||
float camera_pitch_deg = 0.0f;
|
||||
float camera_fov_y_deg = 45.0f;
|
||||
int viewport_w_px = 0;
|
||||
int viewport_h_px = 0;
|
||||
int device_pixel_ratio = 1;
|
||||
};
|
||||
|
||||
#include "OverlayFrame.h"
|
||||
#include "SectionPlane.h"
|
||||
|
||||
// All viewport overlays in one place: axis indicator (corner + pivot),
|
||||
|
||||
@@ -4711,3 +4711,560 @@ void ViewportCore::finalizeScreenshotCapture(WGPUBuffer capture_buffer,
|
||||
pending_screenshot_quit_ = false;
|
||||
if (quit_after) host_->quit();
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Render loop (#84-x): render()
|
||||
// ===========================================================================
|
||||
|
||||
#include <future>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
#include "Stopwatch.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// degrees → radians. Inline-only, used inside render() for the
|
||||
// focal-length derivation.
|
||||
constexpr float degreesToRadians(float deg) {
|
||||
return deg * float(M_PI) / 180.0f;
|
||||
}
|
||||
|
||||
// Format a float with N decimals into the running Log line. Used to
|
||||
// match GL's per-frame stats output where fixed-precision matters for
|
||||
// side-by-side diffs.
|
||||
std::string fmtF(double v, int prec) {
|
||||
std::ostringstream ss;
|
||||
ss << std::fixed << std::setprecision(prec) << v;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// sRGB → linear, used for the background-clear value so the wgpu
|
||||
// surface (which is sRGB on most backends) renders the same colour as
|
||||
// the GL viewport's GL_FRAMEBUFFER_SRGB-enabled pass.
|
||||
inline float srgbToLinear(float c) {
|
||||
return (c <= 0.04045f) ? (c * (1.0f / 12.92f))
|
||||
: std::pow((c + 0.055f) * (1.0f / 1.055f), 2.4f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ViewportCore::render() {
|
||||
if (!device_ || !queue_ || !surface_) return;
|
||||
|
||||
Stopwatch frame_timer;
|
||||
frame_timer.start();
|
||||
|
||||
// Drain any HiZ async readbacks completed since last frame.
|
||||
if (hiz_enabled_) drainHizReadbacks();
|
||||
|
||||
uploadSelectionFlagsIfDirty();
|
||||
|
||||
WGPUSurfaceTexture surf_tex = {};
|
||||
wgpuSurfaceGetCurrentTexture(surface_, &surf_tex);
|
||||
|
||||
switch (surf_tex.status) {
|
||||
case WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal:
|
||||
case WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal:
|
||||
break;
|
||||
case WGPUSurfaceGetCurrentTextureStatus_Timeout:
|
||||
case WGPUSurfaceGetCurrentTextureStatus_Outdated:
|
||||
case WGPUSurfaceGetCurrentTextureStatus_Lost: {
|
||||
int w = 0, h = 0;
|
||||
host_->framebufferSize(w, h);
|
||||
if (w > 0 && h > 0) configureSurface(w, h);
|
||||
host_->requestFrame();
|
||||
return;
|
||||
}
|
||||
default:
|
||||
Log::warn() << "GetCurrentTexture status " << int(surf_tex.status);
|
||||
return;
|
||||
}
|
||||
|
||||
WGPUTextureView view = wgpuTextureCreateView(surf_tex.texture, nullptr);
|
||||
|
||||
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();
|
||||
Eigen::Matrix4f vp_this_frame;
|
||||
{
|
||||
const Eigen::Vector3f target(camera_target_[0], camera_target_[1], camera_target_[2]);
|
||||
const Eigen::Vector3f eye = orbitEye(camera_target_, camera_distance_,
|
||||
camera_yaw_deg_, camera_pitch_deg_);
|
||||
Eigen::Matrix4f v, p;
|
||||
buildViewProj(v, p);
|
||||
const Eigen::Matrix4f vp = p * v;
|
||||
vp_this_frame = vp;
|
||||
float planes[6][4];
|
||||
extractFrustumPlanes(vp.data(), planes);
|
||||
|
||||
// LOD focal: projected_px = world_radius * focal_px / view_z.
|
||||
const Eigen::Vector3f fwd_q = (target - eye).normalized();
|
||||
const Eigen::Vector3f world_up = (std::abs(camera_pitch_deg_) >= 89.0f)
|
||||
? Eigen::Vector3f(0.0f, 1.0f, 0.0f)
|
||||
: Eigen::Vector3f(0.0f, 0.0f, 1.0f);
|
||||
const Eigen::Vector3f right_q = fwd_q.cross(world_up).normalized();
|
||||
const Eigen::Vector3f up_q = right_q.cross(fwd_q).normalized();
|
||||
const float eye_a[3] = { eye.x(), eye.y(), eye.z() };
|
||||
const float fwd_a[3] = { fwd_q.x(), fwd_q.y(), fwd_q.z() };
|
||||
const float right_a[3] = { right_q.x(), right_q.y(), right_q.z() };
|
||||
const float up_a[3] = { up_q.x(), up_q.y(), up_q.z() };
|
||||
const float focal_px = (configured_h_ > 0)
|
||||
? (0.5f * float(configured_h_)
|
||||
/ std::tan(degreesToRadians(camera_fov_y_deg_) * 0.5f))
|
||||
: 0.0f;
|
||||
|
||||
// Motion detection.
|
||||
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 bool use_motion_threshold =
|
||||
camera_moved && motion_min_pixel_radius_ > min_pixel_radius_;
|
||||
const float effective_min_px =
|
||||
use_motion_threshold ? motion_min_pixel_radius_ : min_pixel_radius_;
|
||||
last_cull_was_motion_ = use_motion_threshold;
|
||||
|
||||
// HiZ stale-VP gate. Strict by default; WGPU_HIZ_MOTION=1 trusts
|
||||
// the stale pyramid across motion.
|
||||
static const bool hiz_trust_stale = []{
|
||||
const char* e = std::getenv("WGPU_HIZ_MOTION");
|
||||
return e && e[0] == '1';
|
||||
}();
|
||||
const bool hiz_vp_matches = hiz_valid_
|
||||
&& (hiz_trust_stale || hiz_vp_ == vp_this_frame);
|
||||
const bool hiz_for_this_frame = hiz_enabled_ && hiz_vp_matches;
|
||||
|
||||
// WGPU_HIZ_TRACE per-frame trace budget arm.
|
||||
static const bool hiz_trace_on = []{
|
||||
const char* e = std::getenv("WGPU_HIZ_TRACE");
|
||||
return e && e[0] == '1';
|
||||
}();
|
||||
if (hiz_trace_on && hiz_for_this_frame) {
|
||||
constexpr int kHizTracePerFrame = 12;
|
||||
hiz_trace_budget_.store(kHizTracePerFrame, std::memory_order_relaxed);
|
||||
Log::info()
|
||||
<< "[hiz trace] frame: vp_match="
|
||||
<< (hiz_vp_ == vp_this_frame ? "exact" : "loose")
|
||||
<< " pyramid_mip0=" << hiz_mip_w_[0] << "x" << hiz_mip_h_[0]
|
||||
<< " budget=" << kHizTracePerFrame;
|
||||
} else if (hiz_trace_on) {
|
||||
hiz_trace_budget_.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
HizOccludedFn hiz_occluded;
|
||||
if (hiz_for_this_frame) {
|
||||
hiz_occluded = [this](const float mn[3], const float mx[3]) {
|
||||
return aabbOccludedByHiz(mn, mx);
|
||||
};
|
||||
}
|
||||
|
||||
if (cull_threads_enabled_) {
|
||||
std::vector<std::pair<std::uint32_t, std::future<std::uint32_t>>> futures;
|
||||
futures.reserve(models_gpu_.size());
|
||||
for (auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden) continue;
|
||||
auto& m_ref = m;
|
||||
futures.emplace_back(mid, std::async(std::launch::async,
|
||||
[this, &m_ref, &planes, &eye_a, &fwd_a, &right_a, &up_a,
|
||||
focal_px, effective_min_px, &hiz_occluded]() {
|
||||
return cullModelCpuCompute(
|
||||
m_ref, planes, eye_a, fwd_a, right_a, up_a,
|
||||
focal_px,
|
||||
effective_min_px, lod1_pixel_threshold_,
|
||||
hiz_occluded);
|
||||
}));
|
||||
}
|
||||
for (auto& [mid, fut] : futures) {
|
||||
hiz_reject_count_ += fut.get();
|
||||
}
|
||||
} else {
|
||||
for (auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden) continue;
|
||||
hiz_reject_count_ += cullModelCpuCompute(
|
||||
m, planes, eye_a, fwd_a, right_a, up_a, focal_px,
|
||||
effective_min_px, lod1_pixel_threshold_,
|
||||
hiz_occluded);
|
||||
}
|
||||
}
|
||||
|
||||
const double cull_compute_ms = double(cull_timer.nsecsElapsed()) / 1e6;
|
||||
Stopwatch upload_timer;
|
||||
upload_timer.start();
|
||||
for (auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden) continue;
|
||||
cullModelCpuUpload(m);
|
||||
for (const auto& c : m.chunks) {
|
||||
last_visible_objects_ += c.total_visible_draws;
|
||||
last_visible_triangles_ += c.total_visible_vertices / 3u;
|
||||
if (c.total_visible_draws > 0) last_sub_draws_ += 1;
|
||||
}
|
||||
}
|
||||
last_cull_compute_ms_ = cull_compute_ms;
|
||||
last_cull_upload_ms_ = double(upload_timer.nsecsElapsed()) / 1e6;
|
||||
}
|
||||
|
||||
const double cull_only_ms = double(cull_timer.nsecsElapsed()) / 1e6;
|
||||
last_cull_ms_ = cull_only_ms;
|
||||
|
||||
Stopwatch stream_timer;
|
||||
stream_timer.start();
|
||||
driveStreamingLoads();
|
||||
const double stream_ms = double(stream_timer.nsecsElapsed()) / 1e6;
|
||||
last_stream_ms_ = stream_ms;
|
||||
|
||||
// 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_ += cull_only_ms;
|
||||
bench_stream_ms_total_ += stream_ms;
|
||||
}
|
||||
|
||||
WGPUCommandEncoder enc = wgpuDeviceCreateCommandEncoder(device_, nullptr);
|
||||
|
||||
WGPURenderPassColorAttachment color = {};
|
||||
color.view = msaa_color_view_;
|
||||
color.resolveTarget = view;
|
||||
color.loadOp = WGPULoadOp_Clear;
|
||||
color.storeOp = WGPUStoreOp_Store;
|
||||
color.clearValue = {
|
||||
srgbToLinear(background_color_[0]),
|
||||
srgbToLinear(background_color_[1]),
|
||||
srgbToLinear(background_color_[2]),
|
||||
1.0,
|
||||
};
|
||||
color.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
|
||||
|
||||
WGPURenderPassDepthStencilAttachment depth = {};
|
||||
depth.view = depth_view_;
|
||||
depth.depthLoadOp = WGPULoadOp_Clear;
|
||||
depth.depthStoreOp = WGPUStoreOp_Store;
|
||||
depth.depthClearValue = 1.0f;
|
||||
depth.stencilLoadOp = WGPULoadOp_Undefined;
|
||||
depth.stencilStoreOp = WGPUStoreOp_Undefined;
|
||||
depth.depthReadOnly = false;
|
||||
depth.stencilReadOnly = true;
|
||||
|
||||
WGPURenderPassDescriptor pass_desc = {};
|
||||
pass_desc.colorAttachmentCount = 1;
|
||||
pass_desc.colorAttachments = &color;
|
||||
pass_desc.depthStencilAttachment = depth_view_ ? &depth : nullptr;
|
||||
|
||||
WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(enc, &pass_desc);
|
||||
|
||||
// Two-pass main render: opaque first, then transparent.
|
||||
if (main_pipeline_ && main_pipeline_transparent_
|
||||
&& frame_bind_group_ && !models_gpu_.empty()) {
|
||||
wgpuRenderPassEncoderSetPipeline(pass, main_pipeline_);
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, frame_bind_group_, 0, nullptr);
|
||||
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden) continue;
|
||||
for (const auto& c : m.chunks) {
|
||||
if (!c.bind_group || c.opaque_visible_vertices == 0) continue;
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 1, c.bind_group, 0, nullptr);
|
||||
wgpuRenderPassEncoderDraw(pass,
|
||||
c.opaque_visible_vertices, 1, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
wgpuRenderPassEncoderSetPipeline(pass, main_pipeline_transparent_);
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden) continue;
|
||||
for (const auto& c : m.chunks) {
|
||||
if (!c.bind_group) continue;
|
||||
const std::uint32_t transparent_verts =
|
||||
c.total_visible_vertices - c.opaque_visible_vertices;
|
||||
if (transparent_verts == 0) continue;
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 1, c.bind_group, 0, nullptr);
|
||||
wgpuRenderPassEncoderDraw(pass,
|
||||
transparent_verts, 1,
|
||||
c.opaque_visible_vertices, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the per-frame OverlayFrame snapshot.
|
||||
int viewport_w_px = 0, viewport_h_px = 0;
|
||||
host_->framebufferSize(viewport_w_px, viewport_h_px);
|
||||
const int dpr_int = std::max(1, int(host_->dpr()));
|
||||
|
||||
OverlayFrame overlay_frame;
|
||||
overlay_frame.view_proj = vp_this_frame;
|
||||
overlay_frame.camera_target = Eigen::Vector3f(camera_target_[0],
|
||||
camera_target_[1],
|
||||
camera_target_[2]);
|
||||
overlay_frame.camera_distance = camera_distance_;
|
||||
overlay_frame.camera_yaw_deg = camera_yaw_deg_;
|
||||
overlay_frame.camera_pitch_deg = camera_pitch_deg_;
|
||||
overlay_frame.camera_fov_y_deg = camera_fov_y_deg_;
|
||||
overlay_frame.viewport_w_px = viewport_w_px;
|
||||
overlay_frame.viewport_h_px = viewport_h_px;
|
||||
overlay_frame.device_pixel_ratio = dpr_int;
|
||||
|
||||
// In-pass overlays (section gizmos, highlight triangles, pivot,
|
||||
// overlay lines/points). QtViewportHost forwards to overlays_.X().
|
||||
host_->encodeOverlaysInMainPass(pass, overlay_frame);
|
||||
|
||||
wgpuRenderPassEncoderEnd(pass);
|
||||
wgpuRenderPassEncoderRelease(pass);
|
||||
|
||||
// Edge silhouette + HiZ resolve, before the surface-targeted overlays.
|
||||
if (edges_enabled_) encodeEdgePass(enc, view);
|
||||
|
||||
int hiz_submitted_slot = -1;
|
||||
if (hiz_enabled_) hiz_submitted_slot = encodeHizResolve(enc);
|
||||
|
||||
// Post-main overlays (corner axis, marquee, labels) on the resolved
|
||||
// surface. QtViewportHost forwards to overlays_.X().
|
||||
host_->encodeOverlaysPostMain(enc, view, overlay_frame);
|
||||
|
||||
// Optional capture: encode copy on the same command buffer.
|
||||
WGPUBuffer capture_buffer = nullptr;
|
||||
std::uint32_t capture_padded_bpr = 0;
|
||||
const bool want_capture = !pending_screenshot_path_.empty();
|
||||
if (want_capture) {
|
||||
capture_buffer = encodeScreenshotCapture(
|
||||
enc, surf_tex.texture, capture_padded_bpr);
|
||||
}
|
||||
|
||||
WGPUCommandBuffer cmd = wgpuCommandEncoderFinish(enc, nullptr);
|
||||
wgpuQueueSubmit(queue_, 1, &cmd);
|
||||
|
||||
wgpuCommandBufferRelease(cmd);
|
||||
wgpuCommandEncoderRelease(enc);
|
||||
wgpuTextureViewRelease(view);
|
||||
|
||||
if (want_capture) {
|
||||
finalizeScreenshotCapture(capture_buffer, capture_padded_bpr);
|
||||
}
|
||||
|
||||
// ---- FrameStats emission ---------------------------------------------
|
||||
{
|
||||
const double this_frame_ms =
|
||||
double(frame_timer.nsecsElapsed()) / 1e6;
|
||||
frame_time_ms_sum_ -= frame_time_ms_window_[frame_time_ms_head_];
|
||||
frame_time_ms_window_[frame_time_ms_head_] = this_frame_ms;
|
||||
frame_time_ms_sum_ += this_frame_ms;
|
||||
frame_time_ms_head_ = (frame_time_ms_head_ + 1) % FRAME_TIME_WINDOW;
|
||||
if (frame_time_ms_count_ < FRAME_TIME_WINDOW) ++frame_time_ms_count_;
|
||||
const double avg_ms = frame_time_ms_count_ > 0
|
||||
? frame_time_ms_sum_ / double(frame_time_ms_count_)
|
||||
: 0.0;
|
||||
|
||||
std::uint32_t total_obj = 0, total_tri = 0, total_meshes = 0;
|
||||
for (const auto& [mid, mm] : models_gpu_) {
|
||||
total_obj += std::uint32_t(mm.instances.size());
|
||||
total_tri += mm.index_count / 3;
|
||||
total_meshes += std::uint32_t(mm.meshes.size());
|
||||
}
|
||||
|
||||
FrameStats stats;
|
||||
stats.fps = avg_ms > 0.0 ? float(1000.0 / avg_ms) : 0.0f;
|
||||
stats.frame_time_ms = float(avg_ms);
|
||||
stats.total_objects = total_obj;
|
||||
stats.visible_objects = last_visible_objects_;
|
||||
stats.total_triangles = total_tri;
|
||||
stats.visible_triangles = last_visible_triangles_;
|
||||
stats.unique_meshes = total_meshes;
|
||||
std::uint32_t draw_calls = 0;
|
||||
for (const auto& [mid, mm] : models_gpu_) {
|
||||
if (mm.hidden) continue;
|
||||
for (const auto& c : mm.chunks) {
|
||||
if (c.is_resident && c.total_visible_draws > 0) ++draw_calls;
|
||||
}
|
||||
}
|
||||
stats.gl_draw_calls = draw_calls;
|
||||
stats.indirect_sub_draws = last_sub_draws_;
|
||||
host_->onFrameStats(stats);
|
||||
}
|
||||
|
||||
wgpuSurfacePresent(surface_);
|
||||
wgpuTextureRelease(surf_tex.texture);
|
||||
|
||||
if (last_cull_was_motion_) host_->requestFrame();
|
||||
|
||||
// HiZ async readback handoff.
|
||||
if (hiz_enabled_ && hiz_submitted_slot >= 0) {
|
||||
Stopwatch 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;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Interactive heartbeat log --------------------------------------
|
||||
if (bench_total_ == 0) {
|
||||
++interactive_frame_count_;
|
||||
const float ms = float(frame_timer.nsecsElapsed()) / 1e6f;
|
||||
std::uint64_t total_vbo = 0, total_ebo = 0, total_ssbo = 0;
|
||||
std::uint32_t total_instances = 0;
|
||||
std::size_t chunks_total = 0, chunks_resident = 0;
|
||||
std::size_t chunks_frustum_vis = 0, chunks_missing = 0;
|
||||
for (const auto& [mid, mo] : models_gpu_) {
|
||||
total_vbo += mo.vram_bytes_vbo;
|
||||
total_ebo += mo.vram_bytes_ebo;
|
||||
total_ssbo += mo.vram_bytes_ssbo;
|
||||
total_instances += mo.instance_count;
|
||||
for (const auto& c : mo.chunks) {
|
||||
++chunks_total;
|
||||
if (c.is_resident) ++chunks_resident;
|
||||
if (c.frustum_visible_count > 0) {
|
||||
++chunks_frustum_vis;
|
||||
if (!c.is_resident) ++chunks_missing;
|
||||
}
|
||||
}
|
||||
}
|
||||
const double mb = 1.0 / (1024.0 * 1024.0);
|
||||
Log::info()
|
||||
<< "[frame] " << fmtF(ms > 0 ? 1000.0f / ms : 0.0f, 1) << " fps"
|
||||
<< " " << fmtF(ms, 2) << " ms"
|
||||
<< " obj " << last_visible_objects_ << "/" << total_instances
|
||||
<< " tri " << last_visible_triangles_
|
||||
<< " sub_draws " << last_sub_draws_
|
||||
<< " hiz_rej " << hiz_reject_count_
|
||||
<< " cull " << fmtF(last_cull_ms_, 2) << "ms"
|
||||
<< " stream " << fmtF(last_stream_ms_, 2) << "ms"
|
||||
<< " chunks " << chunks_resident << "/" << chunks_frustum_vis
|
||||
<< "/" << chunks_total << " (missing " << chunks_missing << ")"
|
||||
<< " vram " << fmtF(double(total_vbo + total_ebo + total_ssbo) * mb, 1) << "MB"
|
||||
<< " models " << models_gpu_.size()
|
||||
<< " lod1 " << lod1_dbg_count_ << "/" << (lod1_dbg_count_ + lod0_dbg_eligible_count_)
|
||||
<< " (saved " << lod1_dbg_tris_saved_ << " tris, "
|
||||
<< lod0_dbg_no_lod1_count_ << " no-lod1)";
|
||||
lod1_dbg_count_ = 0;
|
||||
lod0_dbg_eligible_count_ = 0;
|
||||
lod0_dbg_no_lod1_count_ = 0;
|
||||
lod1_dbg_tris_saved_ = 0;
|
||||
}
|
||||
|
||||
// ---- Benchmark integration + auto-quit -------------------------------
|
||||
if (bench_total_ > 0) {
|
||||
if (!bench_warm_done_) {
|
||||
constexpr int CONVERGE_FRAMES_REQUIRED = 5;
|
||||
constexpr int MAX_WARM_FRAMES = 600;
|
||||
const bool worker_idle =
|
||||
streaming_thread_.inFlightApprox() == 0;
|
||||
if (streaming_loads_this_frame_ > 0 || !worker_idle) {
|
||||
bench_warm_streak_ = 0;
|
||||
} else {
|
||||
++bench_warm_streak_;
|
||||
}
|
||||
++bench_warm_frames_total_;
|
||||
const bool converged = bench_warm_streak_ >= CONVERGE_FRAMES_REQUIRED;
|
||||
const bool timed_out = bench_warm_frames_total_ >= MAX_WARM_FRAMES;
|
||||
if (converged) {
|
||||
Log::info() << "[bench warm] converged after "
|
||||
<< bench_warm_frames_total_ << " frames";
|
||||
bench_warm_done_ = true;
|
||||
} else if (timed_out) {
|
||||
Log::warn() << "[bench warm] timed out after "
|
||||
<< bench_warm_frames_total_
|
||||
<< " frames without convergence; starting bench anyway";
|
||||
bench_warm_done_ = true;
|
||||
} else {
|
||||
host_->requestFrame();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const float ms = float(frame_timer.nsecsElapsed()) / 1e6f;
|
||||
|
||||
if (bench_count_ >= bench_warmup_) {
|
||||
bench_frame_ms_.push_back(ms);
|
||||
}
|
||||
|
||||
if ((bench_count_ % 50) == 0) {
|
||||
std::uint64_t total_vbo = 0, total_ebo = 0, total_ssbo = 0;
|
||||
std::uint32_t total_instances = 0;
|
||||
for (const auto& [mid, mo] : models_gpu_) {
|
||||
total_vbo += mo.vram_bytes_vbo;
|
||||
total_ebo += mo.vram_bytes_ebo;
|
||||
total_ssbo += mo.vram_bytes_ssbo;
|
||||
total_instances += mo.instance_count;
|
||||
}
|
||||
const double mb = 1.0 / (1024.0 * 1024.0);
|
||||
const double avg_n = double(std::max(1, bench_count_ - bench_warmup_ + 1));
|
||||
const double cull_ms = bench_cull_ms_total_ / avg_n;
|
||||
const double stream_ms2 = bench_stream_ms_total_ / avg_n;
|
||||
Log::info()
|
||||
<< "[frame] " << fmtF(ms > 0 ? 1000.0f / ms : 0.0f, 1) << " fps"
|
||||
<< " " << fmtF(ms, 2) << " ms"
|
||||
<< " obj " << last_visible_objects_ << "/" << total_instances
|
||||
<< " tri " << last_visible_triangles_
|
||||
<< " sub_draws " << last_sub_draws_
|
||||
<< " hiz_rej " << hiz_reject_count_
|
||||
<< " cull[wall " << fmtF(cull_ms, 2)
|
||||
<< " | compute " << fmtF(last_cull_compute_ms_, 2)
|
||||
<< " upload " << fmtF(last_cull_upload_ms_, 2) << "]ms"
|
||||
<< " stream[" << fmtF(stream_ms2, 2) << "]ms"
|
||||
<< " vram " << fmtF(double(total_vbo + total_ebo + total_ssbo) * mb, 1) << "MB"
|
||||
<< " models " << models_gpu_.size()
|
||||
<< " lod1 " << lod1_dbg_count_ << "/" << (lod1_dbg_count_ + lod0_dbg_eligible_count_)
|
||||
<< " (saved " << lod1_dbg_tris_saved_ << " tris, "
|
||||
<< lod0_dbg_no_lod1_count_ << " no-lod1)";
|
||||
lod1_dbg_count_ = 0;
|
||||
lod0_dbg_eligible_count_ = 0;
|
||||
lod0_dbg_no_lod1_count_ = 0;
|
||||
lod1_dbg_tris_saved_ = 0;
|
||||
}
|
||||
camera_yaw_deg_ = bench_yaw_start_
|
||||
+ bench_yaw_speed_ * float(bench_count_ + 1);
|
||||
++bench_count_;
|
||||
|
||||
if (bench_count_ >= bench_warmup_ + bench_total_) {
|
||||
std::vector<float> times = bench_frame_ms_;
|
||||
std::sort(times.begin(), times.end());
|
||||
auto pct = [×](double p) -> float {
|
||||
if (times.empty()) return 0.0f;
|
||||
const std::size_t idx = std::min(times.size() - 1,
|
||||
std::size_t(p * double(times.size() - 1)));
|
||||
return times[idx];
|
||||
};
|
||||
float sum = 0.0f;
|
||||
for (float f : times) sum += f;
|
||||
const float avg = times.empty() ? 0.0f : sum / float(times.size());
|
||||
const float median = pct(0.5);
|
||||
const float p1 = pct(0.01);
|
||||
const float p99 = pct(0.99);
|
||||
|
||||
const float total_sweep = bench_yaw_speed_ * float(bench_total_);
|
||||
Log::info() << "\n=== BENCHMARK (" << bench_total_ << " frames, orbit "
|
||||
<< total_sweep << "deg at " << bench_yaw_speed_ << "deg/frame) ===";
|
||||
Log::info() << " avg: " << avg << " ms (" << (avg > 0 ? 1000.0f/avg : 0.0f) << " fps)";
|
||||
Log::info() << " median: " << median << " ms (" << (median > 0 ? 1000.0f/median : 0.0f) << " fps)";
|
||||
Log::info() << " p1: " << p1 << " ms p99: " << p99 << " ms";
|
||||
Log::info() << " last frame: obj " << last_visible_objects_
|
||||
<< " tri " << last_visible_triangles_
|
||||
<< " sub_draws " << last_sub_draws_
|
||||
<< " hiz_rej " << hiz_reject_count_;
|
||||
const double n = double(std::max(1, bench_total_));
|
||||
Log::info() << " per-frame avg ms: cull=" << bench_cull_ms_total_ / n
|
||||
<< " stream=" << bench_stream_ms_total_ / n
|
||||
<< " hiz_readback=" << bench_hiz_readback_ms_total_ / n
|
||||
<< " hiz=" << (hiz_enabled_ ? "on" : "off");
|
||||
Log::info() << "=== END BENCHMARK ===\n";
|
||||
|
||||
bench_total_ = 0;
|
||||
host_->quit();
|
||||
} else {
|
||||
host_->requestFrame();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -47,6 +47,9 @@
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "FrameStats.h"
|
||||
#include "OverlayFrame.h"
|
||||
|
||||
class ViewportHost {
|
||||
public:
|
||||
virtual ~ViewportHost() = default;
|
||||
@@ -91,12 +94,23 @@ public:
|
||||
int /*modifiers*/) {}
|
||||
virtual void onToolModeChanged(int /*tool_mode*/) {}
|
||||
virtual void onToolBackspacePressed() {}
|
||||
// FrameStats is a Qt-using struct declared inside ViewportWindow
|
||||
// today; once the move to ViewportCore happens it'll come back here
|
||||
// as a plain POD. For B4 the desktop ViewportWindow keeps the
|
||||
// existing typed signal and this is just the placeholder.
|
||||
virtual void onFrameStats(double /*frame_ms*/, double /*cull_ms*/,
|
||||
uint32_t /*draws*/, uint32_t /*tris*/) {}
|
||||
|
||||
// Per-frame statistics. Fired once per render() after the main pass
|
||||
// is encoded; QtViewportHost forwards to `emit frameStatsUpdated(...)`.
|
||||
virtual void onFrameStats(const FrameStats& /*stats*/) {}
|
||||
|
||||
// Overlay encode hooks. ViewportCore::render() calls these mid-
|
||||
// frame so the Qt-bound OverlayRenderer (which carries QString
|
||||
// labels for the HUD) can encode its passes without core having
|
||||
// to include OverlayRenderer.h. `inMainPass` runs inside the MSAA
|
||||
// pass (section gizmos, highlight triangles, pivot, overlay
|
||||
// lines/points); `postMain` runs on the resolved surface after
|
||||
// the edge pass (corner axis, marquee, labels).
|
||||
virtual void encodeOverlaysInMainPass(WGPURenderPassEncoder /*pass*/,
|
||||
const OverlayFrame& /*frame*/) {}
|
||||
virtual void encodeOverlaysPostMain(WGPUCommandEncoder /*enc*/,
|
||||
WGPUTextureView /*surface_view*/,
|
||||
const OverlayFrame& /*frame*/) {}
|
||||
|
||||
// Encode + save the screenshot. Called from the render path after
|
||||
// wgpu has mapped the surface-copy staging buffer back to host
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -39,6 +39,7 @@
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "FrameStats.h"
|
||||
#include "SidecarCache.h"
|
||||
#include "BufferPool.h"
|
||||
#include "InstanceCompose.h"
|
||||
@@ -88,6 +89,12 @@ public:
|
||||
void onSurfacePickedInTool(int x_px, int y_px, int modifiers) override;
|
||||
void onToolModeChanged(int tool_mode) override;
|
||||
void onToolBackspacePressed() override;
|
||||
void onFrameStats(const FrameStats& stats) override;
|
||||
void encodeOverlaysInMainPass(WGPURenderPassEncoder pass,
|
||||
const OverlayFrame& frame) override;
|
||||
void encodeOverlaysPostMain(WGPUCommandEncoder enc,
|
||||
WGPUTextureView surface_view,
|
||||
const OverlayFrame& frame) override;
|
||||
void saveScreenshotRgba8(const std::string& path, const std::uint8_t* rgba,
|
||||
int w, int h) override;
|
||||
|
||||
@@ -463,17 +470,11 @@ public:
|
||||
// ViewportWindow::FrameStats so bonsai's status bar binding ports
|
||||
// unchanged. gl_draw_calls is the wgpu draw-call count (named for
|
||||
// continuity with the GL field bonsai's status format string uses).
|
||||
struct FrameStats {
|
||||
float fps;
|
||||
float frame_time_ms;
|
||||
uint32_t total_objects;
|
||||
uint32_t visible_objects;
|
||||
uint32_t total_triangles;
|
||||
uint32_t visible_triangles;
|
||||
uint32_t unique_meshes;
|
||||
uint32_t gl_draw_calls; // wgpu draw-call count; name kept for bonsai parity
|
||||
uint32_t indirect_sub_draws; // sub-draws packed into the chunk-indirect lists
|
||||
};
|
||||
// ViewportWindow::FrameStats is a using-alias for ::FrameStats (moved
|
||||
// to its own Qt-free header so ViewportHost::onFrameStats can carry
|
||||
// it without dragging Qt into core). Existing bonsai signal binding
|
||||
// (frameStatsUpdated) keeps using the qualified name.
|
||||
using FrameStats = ::FrameStats;
|
||||
|
||||
signals:
|
||||
// Selection moved by a pick / marquee. Emitted with the active id
|
||||
@@ -830,14 +831,16 @@ private:
|
||||
// rate matches GL's; on the federation scene this lands obj/tri
|
||||
// counts within ~10% of GL's across an orbit (vs ~3× without the
|
||||
// bump). Override at runtime via WGPU_MIN_PX / WGPU_MIN_PX_MOTION.
|
||||
float min_pixel_radius_ = 3.0f;
|
||||
float motion_min_pixel_radius_ = 15.0f;
|
||||
// Cull-tuning aliases (storage in core_, #84-x). VW's env-var prelude
|
||||
// still mutates these through the alias.
|
||||
float& min_pixel_radius_;
|
||||
float& motion_min_pixel_radius_;
|
||||
|
||||
// Whether driveCull dispatches per-model work via std::async. ON by
|
||||
// default; setting WGPU_CULL_THREADS=0 forces sequential cull for
|
||||
// measurement (does std::async actually parallelize on this libstdc++?
|
||||
// and is per-model the right granularity?).
|
||||
bool cull_threads_enabled_ = true;
|
||||
bool& cull_threads_enabled_;
|
||||
|
||||
public:
|
||||
// Master switch for HiZ occlusion. OFF by default — has two issues vs
|
||||
@@ -893,16 +896,14 @@ public:
|
||||
// 0 loads (convergence) before starting the orbit sweep, capped by
|
||||
// MAX_WARM_FRAMES so chronically thrashing scenes still produce
|
||||
// numbers. Both reset implicitly per bench run via setBenchmarkFrames.
|
||||
int bench_warm_streak_ = 0;
|
||||
int bench_warm_frames_total_ = 0;
|
||||
bool bench_warm_done_ = false; // latch: once true, gate is open for this run
|
||||
int& bench_warm_streak_;
|
||||
int& bench_warm_frames_total_;
|
||||
bool& bench_warm_done_;
|
||||
|
||||
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.
|
||||
float lod1_pixel_threshold_ = 30.0f;
|
||||
// LOD1 pixel threshold alias (storage in core_, #84-x).
|
||||
float& lod1_pixel_threshold_;
|
||||
|
||||
// Per-model state aliases (storage in core_).
|
||||
std::unordered_map<uint32_t, ModelGpuData>& models_gpu_;
|
||||
@@ -917,22 +918,13 @@ private:
|
||||
// alias so VW::setCamera can flip it without poking through core_.
|
||||
bool& initial_view_applied_;
|
||||
|
||||
// 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;
|
||||
|
||||
// True iff the last cull used the motion threshold. Render schedules a
|
||||
// single settle frame after motion stops so the previously dropped
|
||||
// sub-pixel instances reappear at the still threshold. Without this,
|
||||
// event-driven rendering would leave those instances missing forever
|
||||
// because no further frame is requested after the user releases the
|
||||
// mouse. Matches GL's last_cull_was_motion_ behaviour.
|
||||
bool last_cull_was_motion_ = false;
|
||||
// Camera-state aliases for motion detection (storage in core_, #84-x).
|
||||
float (&prev_camera_target_)[3];
|
||||
float& prev_camera_distance_;
|
||||
float& prev_camera_yaw_deg_;
|
||||
float& prev_camera_pitch_deg_;
|
||||
bool& has_prev_camera_;
|
||||
bool& last_cull_was_motion_;
|
||||
|
||||
// Pending one-shot screenshot path alias (storage in core_).
|
||||
// Captured at the end of the next render(); driveStreamingLoads
|
||||
@@ -949,51 +941,27 @@ private:
|
||||
Eigen::Vector2i nav_press_pos_;
|
||||
bool nav_dragged_ = false;
|
||||
|
||||
// Benchmark mode. setBenchmarkFrames(N) arms it; render() integrates the
|
||||
// yaw, captures per-frame ms after warmup, and prints + quits when the
|
||||
// target frame count is hit.
|
||||
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_;
|
||||
|
||||
// Per-frame stat snapshot from the last cull. Sum of m.mesh_draws across
|
||||
// visible models. Exposed via the benchmark summary; will grow into a
|
||||
// proper FrameStats signal when stage 11's host integration arrives.
|
||||
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_stream_ms_total_ = 0.0; // driveStreamingLoads only
|
||||
double bench_hiz_readback_ms_total_ = 0.0;
|
||||
double bench_submit_ms_total_ = 0.0;
|
||||
|
||||
// Last-frame per-phase times. Available in interactive mode (no
|
||||
// bench) so the periodic [frame] heartbeat log can show cull /
|
||||
// stream cost without needing the bench averaging machinery.
|
||||
double last_cull_ms_ = 0.0;
|
||||
double last_cull_compute_ms_ = 0.0; // parallel per-model cull
|
||||
double last_cull_upload_ms_ = 0.0; // sequential queueWriteBuffer pass
|
||||
double last_stream_ms_ = 0.0;
|
||||
|
||||
// Tick count for the interactive (non-bench) [frame] heartbeat log.
|
||||
// Increments every render() and prints stats every N frames.
|
||||
int interactive_frame_count_ = 0;
|
||||
|
||||
// Rolling 60-sample frame-time window for the smoothed fps emitted
|
||||
// via frameStatsUpdated. Index wraps; sum kept incrementally to
|
||||
// avoid a per-frame reduction.
|
||||
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 + frame-stats aliases (storage in core_, #84-x).
|
||||
// setBenchmarkFrames(N) writes the bench_* fields through these
|
||||
// aliases; the heartbeat / FrameStats path in core_.render reads
|
||||
// and resets them.
|
||||
int& bench_total_;
|
||||
int& bench_count_;
|
||||
int& bench_warmup_;
|
||||
float& bench_yaw_start_;
|
||||
float& bench_yaw_speed_;
|
||||
std::vector<float>& bench_frame_ms_;
|
||||
uint32_t& last_visible_objects_;
|
||||
uint32_t& last_visible_triangles_;
|
||||
uint32_t& last_sub_draws_;
|
||||
double& bench_cull_ms_total_;
|
||||
double& bench_stream_ms_total_;
|
||||
double& bench_hiz_readback_ms_total_;
|
||||
double& last_cull_ms_;
|
||||
double& last_cull_compute_ms_;
|
||||
double& last_cull_upload_ms_;
|
||||
double& last_stream_ms_;
|
||||
int& interactive_frame_count_;
|
||||
|
||||
// FederatedFalseOrigin matrix, in metres. Default identity. Stored
|
||||
// but not yet applied to per-instance composed transforms — the
|
||||
|
||||
Reference in New Issue
Block a user