ifcviewer: latch the motion contribution cull instead of flip-flopping

The coarse motion threshold (15 px vs the 3 px still floor) followed the
per-frame "did the camera move" test directly. During a slow drag on a
janky main thread — the 66-model web session at 20 fps, mouse events
coalesced — some frames see no camera change, so the cull alternated
between thresholds every few frames: 84% of the visible set vanishing
and reappearing (139k <-> 22k objects in the log), with a full
visible-set re-upload at each flip feeding the very jank that caused the
gaps. On screen it read as the model sporadically jumping and returning
while orbiting slowly, easing as streaming and caching settled — which
is exactly how it was reported.

The motion state now latches: any camera movement arms it, and it only
drops after 250 ms of stillness, with the render loop kept alive over
the hold so the fine-threshold re-cull actually runs in an on-demand
loop. A drag degrades once at its start and restores once shortly after
it ends. Measured with a deliberately gappy scripted drag: two
transitions for the whole drag where each 120 ms pause previously
flipped it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-08-24 11:39:23 +10:00
parent 4a761b51f5
commit ad113e1283
2 changed files with 26 additions and 1 deletions
+13 -1
View File
@@ -7870,8 +7870,20 @@ void ViewportCore::render() {
|| camera_distance_ != prev_camera_distance_
|| camera_yaw_deg_ != prev_camera_yaw_deg_
|| camera_pitch_deg_ != prev_camera_pitch_deg_);
if (camera_moved) {
motion_cull_latched_ = true;
motion_hold_timer_.start();
} else if (motion_cull_latched_
&& motion_hold_timer_.elapsed() >= kMotionHoldMs) {
motion_cull_latched_ = false;
}
// While the latch holds with the camera still, keep frames coming so
// the expiry actually happens and the fine-threshold re-cull runs —
// the loop is on demand, and a fully-resident scene would otherwise
// stay coarsely culled until the next input.
if (motion_cull_latched_ && !camera_moved) host_->requestFrame();
const bool use_motion_threshold =
camera_moved && motion_min_pixel_radius_ > min_pixel_radius_;
motion_cull_latched_ && 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;
+13
View File
@@ -1632,6 +1632,19 @@ private:
double last_cull_compute_ms_ = 0.0;
double last_cull_upload_ms_ = 0.0;
double last_stream_ms_ = 0.0;
// Motion-cull latch. The coarse motion threshold used to follow the
// per-frame "did the camera move" test directly, which flip-flops
// during a slow low-fps drag: coalesced mouse events leave frames
// where the camera happens not to change, so the cull alternated
// between the 3 px and 15 px thresholds — most of the scene vanishing
// and reappearing every few frames, with a full visible-set re-upload
// at each flip. The latch holds the coarse threshold until the camera
// has been still for kMotionHoldMs, so a drag degrades once at its
// start and restores once, shortly after it ends.
static constexpr int kMotionHoldMs = 250;
bool motion_cull_latched_ = false;
Stopwatch motion_hold_timer_;
// 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