Repository navigation
viewer: eliminate UI freeze and slider snap-back on option change - #15029
remibettan merged 10 commits into
Conversation
a39c375 to
bfc3392
Compare
7273b3b to
8dee2d9
Compare
|
Refactored the async-write layer to What changed
Net diff in this commit: −14 LOC across the 5 touched files (175 + / 189 −). Lifetime safety: Let me know if the device-level (vs subdevice-level) variant is preferred — easy to lift the dispatcher one level up if cross-sensor serialization is wanted. |
… blocks on FW round-trip Each option_model::set_option used to call endpoint->set_option synchronously on the UI thread. That call is ~200 ms on UVC and serializes on the per-device USB lock against options_watcher's 1 s poll cycle, so any slider drag or checkbox toggle visibly froze the viewer. Introduces option_async_setter, a per-option worker thread that coalesces posted values (only the latest between two FW writes wins, no stale-value queue), and routes set_option / slider_selected through it. Adds last_user_set_stopwatch on subdevice_model: option_model::set_option_async resets it on every dispatch, and subdevice_model::update() returns early for 500 ms after the last user write so its per-frame get_option_value() polling and the JSON config-save block don't fight the in-flight worker write on the same USB bus. The cached value still refreshes during the gate via options_watcher -> on_options_changed -> update_value. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When _options_invalidated is set, subdevice_model::update() previously called save_processing_block_to_config_file 5x plus once per post-processing model on the UI thread. Each call iterates the block's options and runs config_file::set per option, and every set rewrites the entire JSON config to disk synchronously (see common/rs-config.cpp). For a typical depth+color setup that's tens of full file rewrites per change — ~400 ms of disk-IO blocking the render loop. Adds an anonymous-namespace config_save_worker singleton with one persistent thread. update() now captures the processing-block shared_ptrs by value and posts the save lambda to the worker; the UI thread returns immediately. Posts coalesce by opaque void* key (subdevice identity), so rapid invalidation events collapse into at most one save pass per wake-up cycle. The lambda never dereferences `this` — UAF-safe even if the subdevice is destroyed mid-save. ~subdevice_model still calls cancel(this) to drop any not-yet-started job for tidy shutdown. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… in flight After dispatching an option write asynchronously, the cached option_model::value isn't refreshed until either the options_watcher callback fires (~1 s later) or the post-gate update_all_fields poll reads it back. In the meantime, draw_slider re-reads value_as_float() on the next render frame and the slider visually snaps back to the old value, then jumps to the new value once the echo finally arrives — a regression introduced by making the write async. Adds an optimistic local cache (_optimistic_value + stopwatch) seeded by set_option_async. value_as_float() returns the optimistic value while it's fresh (capped at 2 s in case the FW rejects/clamps and never echoes the requested value), so the slider stays pinned to the user's choice from the moment they release. The flag is cleared as soon as either authoritative path refreshes value: update_value (options_watcher callback) or update_all_fields (per-frame poll). After clearing, value_as_float() returns whatever FW actually applied — so a clamped/rejected value shows real applied state instead of the user's request. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Make _has_optimistic atomic via a shared_ptr<atomic<bool>> wrapper. The flag is read on the UI thread (value_as_float) and written on both the UI thread (set_option_async, update_all_fields) and the on_options_changed callback thread (update_value); shared_ptr keeps option_model copyable since std::atomic deletes its copy/move ctors. Reorder writes in set_option_async so observers that load the flag set also see matching _optimistic_value / _optimistic_stopwatch. - Drop dead error_message / ignore_period parameters from option_model::set_option (now fire-and-forget). Update all call sites accordingly. Reduce slider_unselected to a no-op since the rate-limit retry path it guarded is now unreachable. - Bypass option_model in on_chip_calib_manager: calibration needs synchronous set/verify semantics, so call sensor->set_option / sensor->get_option directly instead of going through the (now async) UI cache. - Log FW errors from option_async_setter::run with LOG_WARNING for rs2::error / std::exception / unknown - silent rejections/clamps were undiagnosable before. - Scope the 500 ms USB-bus contention gate to just the two paths it protects (config save + per-frame option polling) instead of returning early from the entire subdevice_model::update(). Config-file thread-safety (recursive_mutex inside config_file) has been split out to PR realsenseai#15092 so it can be reviewed independently. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
4c60fe6 to
33ec131
Compare
Suggestion: tie invalidate + log to the actual FW write, and floor writes at 200 msRight now the three things below are decoupled from when the FW write actually happens:
The pre-PR behavior was: invalidate + log happened only after
All three should be driven by the worker action. We can reuse the 1. Raise the FW-write floor back to 200 ms in the action (it's currently 50 ms at line 895, not absent): c.try_sleep( std::chrono::milliseconds( 200 ) ); // currently 50 ms2. Extend struct option_async_state
{
std::mutex mutex;
std::string last_error;
bool did_write = false; // a FW write was actually performed
float written_value = 0.f; // the value that was sent to FW
};3. In the worker lambda, record the write on success (next to the existing error handling): if( ! err_msg.empty() )
{
std::lock_guard< std::mutex > lk( state->mutex );
state->last_error = err_msg;
has_user_request->store( false );
}
else
{
std::lock_guard< std::mutex > lk( state->mutex );
state->did_write = true;
state->written_value = v;
}4. Remove the four unconditional UI-thread invalidate lines ( 5. Drain the new flags in std::string async_err;
bool did_write = false;
float written_value = 0.f;
{
std::lock_guard< std::mutex > lk( _async_state->mutex );
async_err.swap( _async_state->last_error );
did_write = _async_state->did_write;
written_value = _async_state->written_value;
_async_state->did_write = false;
}
// ... existing async_err -> error_message handling ...
if( did_write )
{
if( invalidate_flag )
*invalidate_flag = true;
model.add_log( rsutils::string::from() << "Setting " << opt << " to " << written_value );
}This keeps the write to Note: a successful write is also echoed per-option via |
|
Done in 3bfeda3 - followed the plan you laid out almost verbatim:
Result: a 60 Hz slider drag now produces ~5 Hz of FW writes, each producing exactly one invalidate + one log - matching the pre-PR gated cadence. Cross-option re-polls (AE ? exposure/gain) only fire after the FW has actually applied the value. Kept the leading |
Tracked by: RSDSO-20803
Summary
Eliminates a ~1.5 s UI freeze in realsense-viewer on every option change while streaming, plus a slider snap-back regression (slider would briefly jump back to the old cached value before settling on the new one).
Three independent root causes, each fixed in its own commit, plus one peer-review-feedback commit:
Thread-safety for
rs2::config_file(recursive_mutex around all _j / on-disk access) has been split out to #15092 so it can land independently.Each commit compiles independently.
Test plan