Skip to content

Commit 6fdfbd2

Browse files
Evgeni Raikhelclaude
andcommitted
Fix crosshair overlay: video-only, backfill defaults, protect from unrelated saves
Addresses Nir-Az's review comments on PR realsenseai#15093: - Restrict the grid/crosshair toggle button and rendering to 2D video streams (profile.as<rs2::video_stream_profile>()), matching the pattern used elsewhere for stream-details fields. Previously it also rendered on motion (IMU) streams. - Backfill viewer_model.grid_overlay.* defaults into pre-existing, non-empty config files. set_nested_default() already only writes a missing key, so the is_empty() gate around it was redundant and wrong - it meant the grid keys were only ever written into a brand-new config file, never into an existing user's realsense-config.json. Also fixes a related bug found while testing the above: hand-editing viewer_model.grid_overlay.* in realsense-config.json while the viewer is running got silently reverted by the next unrelated config save, because config_file caches the whole document in memory and every set()/set_nested() call blindly overwrites the entire file with that stale copy. Adds config_file::set_protected(), used only by the specific call sites known to fire often enough to have caused this - the window position/size callbacks (drag/resize) and processing-block persistence (stream start, filter enable/disable toggle) - which re-adopt the on-disk grid_overlay section before writing. Every other config_file caller (device options, DDS settings, calibration timestamps, ...) is unaffected and pays no extra I/O. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5cdb43f commit 6fdfbd2

5 files changed

Lines changed: 129 additions & 30 deletions

File tree

common/processing-block-model.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,17 +114,21 @@ namespace rs2
114114
void save_processing_block_to_config_file(const char* name,
115115
std::shared_ptr<rs2::processing_block> pb, bool enable)
116116
{
117+
// set_protected() (not plain set()): this fires on every stream Start (via the
118+
// debounced _options_invalidated path in subdevice_model::update()) and on every
119+
// filter enable/disable toggle - both are frequent enough to have clobbered a
120+
// hand-edited crosshair overlay section, the same way window drag/resize did.
117121
for (auto opt : pb->get_supported_options())
118122
{
119123
auto val = pb->get_option(opt);
120124
std::string key = name;
121125
key += ".";
122126
key += pb->get_option_name(opt);
123-
config_file::instance().set(key.c_str(), val);
127+
config_file::instance().set_protected(key.c_str(), val);
124128
}
125129

126130
std::string key = name;
127131
key += ".enabled";
128-
config_file::instance().set(key.c_str(), enable);
132+
config_file::instance().set_protected(key.c_str(), enable);
129133
}
130134
}

common/rs-config.cpp

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ void config_file::set(const char* key, const char* value)
2222
save();
2323
}
2424

25+
void config_file::set_protected(const char* key, const char* value)
26+
{
27+
std::lock_guard< std::recursive_mutex > lk( _mutex );
28+
_j[key] = value;
29+
save( skip_all );
30+
}
31+
2532
void config_file::set_default(const char* key, const char* calculate)
2633
{
2734
std::lock_guard< std::recursive_mutex > lk( _mutex );
@@ -116,6 +123,47 @@ config_file::config_file( std::string const & filename )
116123

117124
void config_file::save()
118125
{
126+
// Plain write-through, same as before any protected section existed: set()/remove()/
127+
// reset() are used by ~10 unrelated call sites (device options, processing blocks,
128+
// DDS settings, calibration timestamps, ...) that have never been shown to clobber a
129+
// protected section, so they pay no extra disk read. The few call sites that are
130+
// known to (window position/size - see ux-window.cpp) use set_protected() instead.
131+
save( skip_none );
132+
}
133+
134+
void config_file::save( unsigned skip_overwrite_mask )
135+
{
136+
std::lock_guard< std::recursive_mutex > lk( _mutex );
137+
138+
if( skip_overwrite_mask != skip_none && ! _filename.empty() )
139+
{
140+
// _j only reflects what was loaded at startup (plus whatever this process has
141+
// explicitly set since). If the user hand-edited a protected section directly in
142+
// realsense-config.json while the viewer was running, adopt their on-disk values
143+
// for every section named in the mask now - otherwise this save (e.g. triggered by
144+
// an unrelated window move/resize, or on exit) would blindly overwrite the file
145+
// with our stale in-memory copy and silently discard their edit.
146+
try
147+
{
148+
auto on_disk = rsutils::json_config::load_from_file( _filename );
149+
if( on_disk.exists() )
150+
{
151+
if( ( skip_overwrite_mask & skip_grid_overlay )
152+
&& on_disk.contains( "viewer_model" )
153+
&& on_disk["viewer_model"].contains( "grid_overlay" ) )
154+
{
155+
_j["viewer_model"]["grid_overlay"] = on_disk["viewer_model"]["grid_overlay"];
156+
}
157+
// Add an `if (skip_overwrite_mask & skip_<new_section>) ...` block here for
158+
// each additional bit as new protected sections are introduced.
159+
}
160+
}
161+
catch( ... )
162+
{
163+
// Best effort - if the file can't be read, fall back to whatever _j already has.
164+
}
165+
}
166+
119167
save( _filename.c_str() );
120168
}
121169

@@ -139,7 +187,10 @@ config_file& config_file::operator=(const config_file& other)
139187
std::lock_guard< std::recursive_mutex > lk_this( _mutex );
140188
_j = std::move( j_copy );
141189
_defaults = std::move( defaults_copy );
142-
save();
190+
// Assignment is always an intentional, wholesale replacement (Load Settings,
191+
// Restore Defaults + Apply, etc.) - protect nothing here, or we'd immediately
192+
// discard the very values this assignment means to commit.
193+
save( skip_none );
143194
}
144195
return *this;
145196
}

common/rs-config.h

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,21 @@ namespace rs2
7979
set(key, ss.str().c_str());
8080
}
8181

82+
// Same as set(), but the resulting save() also preserves any protected config
83+
// section (currently just the crosshair/grid overlay) from an out-of-process
84+
// hand-edit. Reserved for the few call sites - window position/size - proven to
85+
// fire often enough to have caused exactly that problem; everything else keeps
86+
// using plain set() and pays no extra disk read.
87+
void set_protected(const char* key, const char* value);
88+
89+
template<class T>
90+
void set_protected(const char* key, T val)
91+
{
92+
std::stringstream ss;
93+
ss << val;
94+
set_protected(key, ss.str().c_str());
95+
}
96+
8297
bool contains(const char* key) const;
8398

8499
void save(const char* filename);
@@ -142,6 +157,9 @@ namespace rs2
142157
}
143158

144159
( *current )[keys.back()] = val;
160+
// Plain write-through - set_nested() is also used for e.g. DDS settings, which
161+
// have never been shown to clobber a protected section, so it pays no extra
162+
// disk read. See set_protected() for the call sites that do need it.
145163
save();
146164
}
147165

@@ -190,9 +208,27 @@ namespace rs2
190208
}
191209

192210
private:
211+
// Bitmask of config sections that save() must NOT overwrite with our in-memory
212+
// copy - their on-disk value is kept instead. skip_none (0x0, the default for
213+
// every plain set()/set_nested()/remove()/reset() call) protects nothing, i.e.
214+
// save() writes exactly what's in memory and overwrites the whole file. Only
215+
// set_protected() opts in to skip_all, and only for the call sites proven to
216+
// need it (window position/size - see ux-window.cpp). Add a bit here (and a
217+
// matching branch in save(unsigned)) for each additional config section that
218+
// should survive an out-of-process hand-edit across an unrelated save.
219+
enum skip_overwrite : unsigned
220+
{
221+
skip_none = 0x0,
222+
skip_grid_overlay = 0x1, // viewer_model.grid_overlay.* (crosshair overlay config)
223+
224+
// Bitwise-OR of every bit above. Extend this when a new bit is added.
225+
skip_all = skip_grid_overlay,
226+
};
227+
193228
std::string get_default(const char* key, const char* def) const;
194229

195230
void save();
231+
void save( unsigned skip_overwrite_mask );
196232

197233
// Serializes all reads/writes of `_j` and the on-disk file. Required because
198234
// viewer reads/writes config_file from multiple threads (UI thread, the

common/stream-model.cpp

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -448,10 +448,11 @@ namespace rs2
448448
void stream_model::show_stream_header(ImFont* font, const rect &stream_rect, viewer_model& viewer)
449449
{
450450
const auto top_bar_height = 32.f;
451-
auto num_of_buttons = 6; // Crosshair button is the latest addition
451+
auto num_of_buttons = 5;
452452

453453
if (!viewer.allow_stream_close) --num_of_buttons;
454454
if (viewer.streams.size() > 1) ++num_of_buttons;
455+
if (profile.as<rs2::video_stream_profile>()) ++num_of_buttons; // Grid/crosshair button - video streams only
455456
if (RS2_STREAM_DEPTH == profile.stream_type()) ++num_of_buttons; // Color map ruler button
456457
if (RS2_FORMAT_MOTION_XYZ32F == profile.format()) ++num_of_buttons; // Motion graph button
457458
if (RS2_STREAM_OCCUPANCY == profile.stream_type() && _normalized_zoom.w == 1) ++num_of_buttons; // Safety zones button
@@ -590,29 +591,32 @@ namespace rs2
590591
}
591592
ImGui::SameLine();
592593

593-
label = rsutils::string::from() << textual_icons::grid << "##Grid " << profile.unique_id();
594-
if (show_crosshair)
594+
if (profile.as<rs2::video_stream_profile>()) // Grid/crosshair overlay is only meaningful on 2D video streams
595595
{
596-
ImGui::PushStyleColor(ImGuiCol_Text, light_blue);
597-
ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, light_blue);
598-
if (ImGui::Button(label.c_str(), { 24, top_bar_height }))
596+
label = rsutils::string::from() << textual_icons::grid << "##Grid " << profile.unique_id();
597+
if (show_crosshair)
599598
{
600-
show_crosshair = false;
599+
ImGui::PushStyleColor(ImGuiCol_Text, light_blue);
600+
ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, light_blue);
601+
if (ImGui::Button(label.c_str(), { 24, top_bar_height }))
602+
{
603+
show_crosshair = false;
604+
}
605+
if (ImGui::IsItemHovered())
606+
RsImGui::CustomTooltip("Hide crosshair overlay");
607+
ImGui::PopStyleColor(2);
601608
}
602-
if (ImGui::IsItemHovered())
603-
RsImGui::CustomTooltip("Hide crosshair overlay");
604-
ImGui::PopStyleColor(2);
605-
}
606-
else
607-
{
608-
if (ImGui::Button(label.c_str(), { 24, top_bar_height }))
609+
else
609610
{
610-
show_crosshair = true;
611+
if (ImGui::Button(label.c_str(), { 24, top_bar_height }))
612+
{
613+
show_crosshair = true;
614+
}
615+
if (ImGui::IsItemHovered())
616+
RsImGui::CustomTooltip("Show crosshair/grid overlay");
611617
}
612-
if (ImGui::IsItemHovered())
613-
RsImGui::CustomTooltip("Show crosshair/grid overlay");
618+
ImGui::SameLine();
614619
}
615-
ImGui::SameLine();
616620

617621

618622
if (RS2_STREAM_DEPTH == profile.stream_type())
@@ -2104,7 +2108,7 @@ namespace rs2
21042108

21052109
update_ae_roi_rect(stream_rect, g, error_message);
21062110

2107-
if (show_crosshair)
2111+
if (show_crosshair && profile.as<rs2::video_stream_profile>())
21082112
draw_crosshair(stream_rect, grid_h_lines, grid_v_lines, grid_line_width,
21092113
grid_color_r, grid_color_g, grid_color_b);
21102114

common/ux-window.cpp

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,9 @@ namespace rs2
8787
config_file::instance().set_default(configurations::viewer::commands_xml, "./Commands.xml");
8888
config_file::instance().set_default(configurations::viewer::hwlogger_xml, "./HWLoggerEvents.xml");
8989

90-
if( config_file::instance().is_empty() )
9190
{
91+
// set_nested_default() only writes a key if it's missing, so this is safe to run
92+
// unconditionally - it backfills the grid keys into pre-existing config files too.
9293
namespace cfg = configurations::viewer::viewport_grid_overlay;
9394
auto& cf = config_file::instance();
9495
cf.set_nested_default( cfg::horizontal_lines, 1 );
@@ -372,20 +373,23 @@ namespace rs2
372373
glDebugMessageCallback(MessageCallback, 0);
373374
}
374375

376+
// set_protected() (not plain set()): these fire on every native WM_MOVE/WM_SIZE -
377+
// dozens of times per drag gesture - and are the call sites that motivated
378+
// protecting the crosshair overlay section from an unrelated save (see rs-config.h).
375379
glfwSetWindowPosCallback(_win, [](GLFWwindow* w, int x, int y)
376380
{
377-
config_file::instance().set(configurations::window::saved_pos, true);
378-
config_file::instance().set(configurations::window::position_x, x);
379-
config_file::instance().set(configurations::window::position_y, y);
381+
config_file::instance().set_protected(configurations::window::saved_pos, true);
382+
config_file::instance().set_protected(configurations::window::position_x, x);
383+
config_file::instance().set_protected(configurations::window::position_y, y);
380384
});
381385

382386
glfwSetWindowSizeCallback( _win, []( GLFWwindow * window, int width, int height ) {
383387
if( width > 0 && height > 0 )
384388
{
385-
config_file::instance().set( configurations::window::saved_size, true );
386-
config_file::instance().set( configurations::window::width, width );
387-
config_file::instance().set( configurations::window::height, height );
388-
config_file::instance().set( configurations::window::maximized,
389+
config_file::instance().set_protected( configurations::window::saved_size, true );
390+
config_file::instance().set_protected( configurations::window::width, width );
391+
config_file::instance().set_protected( configurations::window::height, height );
392+
config_file::instance().set_protected( configurations::window::maximized,
389393
glfwGetWindowAttrib( window, GLFW_MAXIMIZED ) );
390394
}
391395
} );

0 commit comments

Comments
 (0)