Web gui parity 2.3 2.5#10678
Conversation
Implements the missing display-control toggles from issue The-OpenROAD-Project#10619, tables 2.3 (instances & cells) and 2.5 (overlays/grids/misc), in the Web GUI: - Block grids & overlays on the transparent overlay tile: GCell grid, Manufacturing grid (>=5px zoom gate), Regions (dbRegion fill), and Access points (green/red "X", zoom + layer gated). - Flight-line overlays as toggles: Flywires (unrouted nets) / Flywires only (all signal nets, capped per tile), and a Focused-nets-guides toggle gating the existing route-guide overlay. - Detailed view (forces sub-5px site rendering), Highlight selected (gates the selection highlight; hover drawn separately). - Tcl display-control persistence mirroring the native GUI: set/check/save/restore_display_controls (WebServer + web.i + web.tcl), with a server->client push applied via the frontend onPush handler. - Polish: scale-bar unit formatting (formatScaleBarLabel) and a Background color picker (overrides --bg-map, persisted in localStorage). Tests are tracked separately and not included in this commit. Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>
Gui::checkDisplayControlsVisible() and the sibling display-control accessors dereferenced the global `main_window` without a null check. In headless/web mode the Qt MainWindow is never created (only the headless viewer hook is set), so `main_window` is null. When the web tile renderer's debug-graphics overlay (drawRendererOverlay, enabled by the "Debug Graphics" toggle) invokes a registered gui::Renderer's drawObjects(), and that renderer calls checkDisplayControl(), the call routes to Gui::checkDisplayControlsVisible() -> main_window->getControls() on a null main_window -> SIGSEGV. Guard these accessors against a null main_window, mirroring the existing guards in register/unregisterRenderer: check* return the default (visible=true, selectable=false), setters and save/restore become no-ops. This makes gui::Renderer::checkDisplayControl() safe in headless/web. Pre-existing crash (reproduces on master with the web "Debug Graphics" overlay enabled); independent of the web display-control parity work. Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>
|
Addresses a portion of #10619 Web GUI: display-control parity with the native GUI (issue #10619, tables 2.3 & 2.5)Closes the pending items of table 2.3 (Display controls — instances & cells) and Legend: ✅ Full (parity) · 🟡 Partial · ❌ Missing · ➕ Web-only · ⬆ changed in this branch 2.3 Display controls — instances & cells
2.5 Display controls — overlays, grids & misc
Additional fix (outside tables 2.3/2.5)
|
There was a problem hiding this comment.
Code Review
This pull request implements display-control state synchronization and overlay rendering for the web GUI, mirroring the Qt GUI's display controls (such as grids, regions, access points, flywires, and detailed view) and adding a background color picker. The review feedback highlights several critical issues: potential out-of-bounds writes and incorrect grid calculations for negative coordinates in the manufacturing grid rendering, program termination bugs caused by using logger_->error instead of logger_->warn for non-fatal file I/O and parsing errors, and incorrect placement of access points for rotated or mirrored instances due to ignoring instance orientation.
| const int first_x = ((dbu_tile.xMin() / grid) + 1) * grid; | ||
| const int last_x = (dbu_tile.xMax() / grid) * grid; | ||
| const int first_y = ((dbu_tile.yMin() / grid) + 1) * grid; | ||
| const int last_y = (dbu_tile.yMax() / grid) * grid; | ||
| for (int gx = first_x; gx <= last_x; gx += grid) { | ||
| const int px = static_cast<int>((gx - dbu_tile.xMin()) * scale); | ||
| for (int gy = first_y; gy <= last_y; gy += grid) { | ||
| const int py = 255 - static_cast<int>((gy - dbu_tile.yMin()) * scale); | ||
| blendPixel(image, px, py, color); | ||
| } | ||
| } |
There was a problem hiding this comment.
The grid start/end calculations (first_x, last_x, etc.) can be incorrect for negative coordinates due to C++ integer division truncation, and they will skip grid lines that lie exactly on the lower boundary of the tile (e.g., when dbu_tile.xMin() % grid == 0). Additionally, there are no bounds checks on px and py before calling blendPixel, which can lead to out-of-bounds writes (and potential crashes/memory corruption) if coordinates fall on the upper boundary (e.g., px = 256) or outside the tile.
Using std::ceil and std::floor provides a robust way to calculate the grid lines for both positive and negative coordinates, and adding explicit bounds checks ensures memory safety.
| const int first_x = ((dbu_tile.xMin() / grid) + 1) * grid; | |
| const int last_x = (dbu_tile.xMax() / grid) * grid; | |
| const int first_y = ((dbu_tile.yMin() / grid) + 1) * grid; | |
| const int last_y = (dbu_tile.yMax() / grid) * grid; | |
| for (int gx = first_x; gx <= last_x; gx += grid) { | |
| const int px = static_cast<int>((gx - dbu_tile.xMin()) * scale); | |
| for (int gy = first_y; gy <= last_y; gy += grid) { | |
| const int py = 255 - static_cast<int>((gy - dbu_tile.yMin()) * scale); | |
| blendPixel(image, px, py, color); | |
| } | |
| } | |
| const int first_x = static_cast<int>(std::ceil(static_cast<double>(dbu_tile.xMin()) / grid)) * grid; | |
| const int last_x = static_cast<int>(std::floor(static_cast<double>(dbu_tile.xMax()) / grid)) * grid; | |
| const int first_y = static_cast<int>(std::ceil(static_cast<double>(dbu_tile.yMin()) / grid)) * grid; | |
| const int last_y = static_cast<int>(std::floor(static_cast<double>(dbu_tile.yMax()) / grid)) * grid; | |
| for (int gx = first_x; gx <= last_x; gx += grid) { | |
| const int px = static_cast<int>((gx - dbu_tile.xMin()) * scale); | |
| if (px < 0 || px >= 256) { | |
| continue; | |
| } | |
| for (int gy = first_y; gy <= last_y; gy += grid) { | |
| const int py = 255 - static_cast<int>((gy - dbu_tile.yMin()) * scale); | |
| if (py < 0 || py >= 256) { | |
| continue; | |
| } | |
| blendPixel(image, px, py, color); | |
| } | |
| } |
| logger_->error(utl::WEB, 50, "Cannot open file for writing: {}", filename); | ||
| return; |
There was a problem hiding this comment.
In OpenROAD, calls to logger_->error terminate the program. Using it here for a non-fatal file writing error will crash the entire application (including the web server or interactive session) instead of gracefully returning as intended by the return; statement. Use logger_->warn instead to report the issue without terminating the process.
| logger_->error(utl::WEB, 50, "Cannot open file for writing: {}", filename); | |
| return; | |
| logger_->warn(utl::WEB, 50, "Cannot open file for writing: {}", filename); | |
| return; |
References
- Calls to logger_->error are expected to terminate the program, so additional error handling like returning an error code or throwing an exception is not necessary.
| logger_->error(utl::WEB, 51, "Cannot open file for reading: {}", filename); | ||
| return false; | ||
| } | ||
| std::ostringstream ss; | ||
| ss << in.rdbuf(); | ||
| boost::json::value parsed; | ||
| try { | ||
| parsed = boost::json::parse(ss.str()); | ||
| } catch (const std::exception& e) { | ||
| logger_->error(utl::WEB, 52, "Invalid display controls file: {}", e.what()); | ||
| return false; | ||
| } | ||
| if (!parsed.is_object()) { | ||
| logger_->error(utl::WEB, 53, "Display controls file is not a JSON object"); | ||
| return false; |
There was a problem hiding this comment.
Using logger_->error here will terminate the program on any non-fatal file reading or JSON parsing error, completely bypassing the recovery code (return false;). Use logger_->warn instead to log the warning and allow the function to return false gracefully.
| logger_->error(utl::WEB, 51, "Cannot open file for reading: {}", filename); | |
| return false; | |
| } | |
| std::ostringstream ss; | |
| ss << in.rdbuf(); | |
| boost::json::value parsed; | |
| try { | |
| parsed = boost::json::parse(ss.str()); | |
| } catch (const std::exception& e) { | |
| logger_->error(utl::WEB, 52, "Invalid display controls file: {}", e.what()); | |
| return false; | |
| } | |
| if (!parsed.is_object()) { | |
| logger_->error(utl::WEB, 53, "Display controls file is not a JSON object"); | |
| return false; | |
| logger_->warn(utl::WEB, 51, "Cannot open file for reading: {}", filename); | |
| return false; | |
| } | |
| std::ostringstream ss; | |
| ss << in.rdbuf(); | |
| boost::json::value parsed; | |
| try { | |
| parsed = boost::json::parse(ss.str()); | |
| } catch (const std::exception& e) { | |
| logger_->warn(utl::WEB, 52, "Invalid display controls file: {}", e.what()); | |
| return false; | |
| } | |
| if (!parsed.is_object()) { | |
| logger_->warn(utl::WEB, 53, "Display controls file is not a JSON object"); | |
| return false; |
References
- Calls to logger_->error are expected to terminate the program, so additional error handling like returning an error code or throwing an exception is not necessary.
| int x = 0, y = 0; | ||
| inst->getLocation(x, y); | ||
| const odb::dbTransform xform({x, y}); |
There was a problem hiding this comment.
Using inst->getLocation(x, y) and constructing a translation-only transform ignores the instance's orientation (rotation and mirroring). This will cause access points for rotated or mirrored instances to be drawn in the wrong locations. Use inst->getTransform() directly to correctly handle both translation and orientation.
| int x = 0, y = 0; | |
| inst->getLocation(x, y); | |
| const odb::dbTransform xform({x, y}); | |
| const odb::dbTransform xform = inst->getTransform(); |
There was a problem hiding this comment.
On the access-points suggestion (inst->getLocation() → inst->getTransform()): this one is intentional, and I've left it as-is. drawAccessPoints here mirrors the native Qt GUI's RenderThread::drawAccessPoints (src/gui/src/renderThread.cpp), which uses inst->getLocation(x, y) + a translation-only dbTransform({x, y}). The access-point coordinate returned by dbITerm::getPrefAccessPoints() is already stored in the instance's
oriented frame, so only the translation to block coordinates is needed. Switching to
inst->getTransform() (which also applies rotation/mirroring) would apply the orientation a second time and misplace access points for rotated/mirrored instances — the opposite of
the intended fix. Keeping getLocation() preserves parity with the native GUI's rendering.
…alc) - saveDisplayControls/restoreDisplayControls: use logger_->warn instead of logger_->error for non-fatal file I/O and JSON parse failures. error() terminates the command and bypassed the return/return false recovery; warn keeps the web server / Tcl session alive and returns gracefully. - drawManufacturingGrid: compute first/last grid lines with std::ceil/std::floor on doubles instead of integer division, so lines exactly on the tile's lower boundary aren't skipped and negative coordinates are handled correctly (integer division truncates toward zero). blendPixel() already bounds-checks px/py, so no out-of-bounds write was possible and no extra guard is added. Addresses the gemini-code-assist review on PR The-OpenROAD-Project#10678. The access-point orientation suggestion is intentionally not applied: drawAccessPoints mirrors the native GUI (renderThread.cpp uses inst->getLocation()+translation, since the access-point coordinate is already oriented); applying inst->getTransform() would double-apply orientation and misplace points for rotated/mirrored cells. Signed-off-by: Jorge Ferreira <jorge.ferreira@precisioninno.com>
|
There is no checking for invalid controls |



No description provided.