From cb70ba89aa41b78d1cf56f6e2be93210cb5ee1c9 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Mon, 7 Sep 2026 11:50:13 -0700 Subject: [PATCH] cabana: smooth startup layout (#38794) * cabana: smooth startup layout * cabana: remove camera tab startup comment * cabana: initialize dock geometry on the first frame * cabana: remove route loading placeholder --- openpilot/tools/cabana/ui/app.cc | 4 +++ openpilot/tools/cabana/ui/mainwin.cc | 29 ++++++++++++------- .../tools/cabana/ui/widgets/cameraview.cc | 2 +- .../tools/cabana/ui/widgets/cameraview.h | 2 ++ .../tools/cabana/ui/widgets/videowidget.cc | 14 +++++++-- 5 files changed, 37 insertions(+), 14 deletions(-) diff --git a/openpilot/tools/cabana/ui/app.cc b/openpilot/tools/cabana/ui/app.cc index ce4678f223..3f4dac1016 100644 --- a/openpilot/tools/cabana/ui/app.cc +++ b/openpilot/tools/cabana/ui/app.cc @@ -132,6 +132,8 @@ public: #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE); #endif + // Restore geometry and render the initial layout before mapping the window. + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); window_ = glfwCreateWindow(1600, 900, "Cabana", nullptr, nullptr); if (window_ == nullptr) { glfwTerminate(); @@ -212,6 +214,8 @@ int run(std::unique_ptr stream, StreamLoader stream_loader, cons inistate::applyWindowGeometry(glfw.window()); MainWindow win(glfw.window(), std::move(stream), std::move(stream_loader), dbc_file); + renderFrame(glfw.window(), &win); + glfwShowWindow(glfw.window()); while (!win.exited()) { if (g_signal_exit.exchange(false)) { printf("\nexiting...\n"); diff --git a/openpilot/tools/cabana/ui/mainwin.cc b/openpilot/tools/cabana/ui/mainwin.cc index e5490432a2..d78de4be34 100644 --- a/openpilot/tools/cabana/ui/mainwin.cc +++ b/openpilot/tools/cabana/ui/mainwin.cc @@ -184,7 +184,7 @@ void MainWindow::drawMenuBar() { if (ImGui::MenuItem("Full Screen", "Ctrl+F11")) toggleFullScreen(); ImGui::Separator(); ImGui::MenuItem(messages_widget_ ? messages_widget_->title().c_str() : "MESSAGES", nullptr, &messages_visible_); - ImGui::MenuItem(video_dock_title_.empty() ? "##video_dock" : video_dock_title_.c_str(), nullptr, &video_visible_); + ImGui::MenuItem(video_dock_title_.empty() ? "Video" : video_dock_title_.c_str(), nullptr, &video_visible_); ImGui::Separator(); if (ImGui::MenuItem("Reset Window Layout")) { messages_visible_ = video_visible_ = true; @@ -789,8 +789,10 @@ void MainWindow::drawWaitDialog() { void MainWindow::drawDockspace() { const ImGuiViewport *viewport = ImGui::GetMainViewport(); - ImGui::SetNextWindowPos(viewport->WorkPos); - ImGui::SetNextWindowSize(viewport->WorkSize); + // Use the menu bar's current-frame reservation, including on the first frame. + const ImRect work_rect = static_cast(viewport)->GetBuildWorkRect(); + ImGui::SetNextWindowPos(work_rect.Min); + ImGui::SetNextWindowSize(work_rect.GetSize()); ImGui::SetNextWindowViewport(viewport->ID); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); @@ -813,6 +815,7 @@ void MainWindow::drawDockspace() { // messages left, video (with charts) right, center widget in the middle ImGui::DockBuilderRemoveNode(dock_id); ImGui::DockBuilderAddNode(dock_id, ImGuiDockNodeFlags_DockSpace); + ImGui::DockBuilderSetNodePos(dock_id, ImGui::GetCursorScreenPos()); ImGui::DockBuilderSetNodeSize(dock_id, dock_size); ImGuiID center = dock_id, left = 0, right = 0; ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.28f, &left, ¢er); @@ -856,11 +859,13 @@ bool beginPanel(const char *name, bool *open, ImGuiWindowFlags flags = 0) { } // namespace void MainWindow::drawMessagesPanel() { - const std::string name = messages_widget_->title() + MESSAGES_PANEL_ID; + const std::string name = (messages_widget_ ? messages_widget_->title() : "MESSAGES") + std::string(MESSAGES_PANEL_ID); setNextPanelClass(); if (beginPanel(name.c_str(), &messages_visible_)) { - help_overlay_.add(messages_widget_->whatsThis(), ImGui::GetCurrentWindow()->Rect()); - messages_widget_->draw(); + if (messages_widget_) { + help_overlay_.add(messages_widget_->whatsThis(), ImGui::GetCurrentWindow()->Rect()); + messages_widget_->draw(); + } } const bool floating = floatingOut(); ImGui::End(); @@ -868,13 +873,13 @@ void MainWindow::drawMessagesPanel() { } void MainWindow::drawVideoPanel() { - const std::string name = video_dock_title_ + VIDEO_PANEL; + const std::string name = (video_dock_title_.empty() ? "Video" : video_dock_title_) + VIDEO_PANEL; setNextPanelClass(); const bool video_open = beginPanel(name.c_str(), &video_visible_); const bool floating = floatingOut(); - if (!video_open) { + if (video_widget_ && !video_open) { video_widget_->setVisible(false); // the dock is collapsed or tabbed behind another one, like hideEvent - } else { + } else if (video_widget_) { const ImVec2 avail = ImGui::GetContentRegionAvail(); const bool live = can->liveStreaming(); // the bordered child pads its content, so the heights the widget asks for grow by the padding @@ -962,9 +967,11 @@ void MainWindow::draw() { ImGui::EndChild(); } ImGui::End(); - if (messages_widget_ && messages_visible_) drawMessagesPanel(); + // Submit the same dock windows while loading, so ImGui doesn't collapse their + // nodes and then redistribute the layout when the stream's widgets arrive. + if (messages_visible_) drawMessagesPanel(); if (video_widget_ && !video_visible_) video_widget_->setVisible(false); - if (video_widget_ && video_visible_) drawVideoPanel(); + if (video_visible_) drawVideoPanel(); if (charts_widget_ && charts_floating_) { bool open = true; ImGui::SetNextWindowSize(ImGui::GetMainViewport()->WorkSize, ImGuiCond_Appearing); diff --git a/openpilot/tools/cabana/ui/widgets/cameraview.cc b/openpilot/tools/cabana/ui/widgets/cameraview.cc index 1f730344d5..460ef47c25 100644 --- a/openpilot/tools/cabana/ui/widgets/cameraview.cc +++ b/openpilot/tools/cabana/ui/widgets/cameraview.cc @@ -95,7 +95,7 @@ float CameraWidget::frameAspectRatio() const { if (frame_texture_.width > 0 && frame_texture_.height > 0) { return (float)frame_texture_.width / frame_texture_.height; } - return 1928.0f / 1208.0f; // the road camera, until the first frame arrives + return DEFAULT_CAMERA_ASPECT_RATIO; // the road camera, until the first frame arrives } void CameraWidget::paint() { diff --git a/openpilot/tools/cabana/ui/widgets/cameraview.h b/openpilot/tools/cabana/ui/widgets/cameraview.h index 2812aea86e..a90565a0f8 100644 --- a/openpilot/tools/cabana/ui/widgets/cameraview.h +++ b/openpilot/tools/cabana/ui/widgets/cameraview.h @@ -18,6 +18,8 @@ #include "tools/cabana/core/observable.h" #include "msgq/visionipc/visionipc_client.h" +constexpr float DEFAULT_CAMERA_ASPECT_RATIO = 1928.0f / 1208.0f; + // Center-crop the source to fill the destination without stretching. inline ImVec2 videoFillUv(const ImVec2 &size, float aspect_ratio) { const float ratio = size.x / size.y / aspect_ratio; diff --git a/openpilot/tools/cabana/ui/widgets/videowidget.cc b/openpilot/tools/cabana/ui/widgets/videowidget.cc index 91b41184c0..ce8fb64c04 100644 --- a/openpilot/tools/cabana/ui/widgets/videowidget.cc +++ b/openpilot/tools/cabana/ui/widgets/videowidget.cc @@ -261,6 +261,16 @@ void VideoWidget::createCameraWidget() { if (index != -1) cam_widget_->setStreamType((VisionStreamType)camera_tab_->tabData(index)); })); connections_.push_back(static_cast(can)->qLogLoaded.connect([this](std::shared_ptr qlog) { cam_widget_->parseQLog(qlog); })); + + if (auto *replay = getReplay(); replay && !replay->hasFlag(REPLAY_FLAG_NO_VIPC)) { + std::set streams; + for (const auto &[num, segment] : replay->route().segments()) { + if (!segment.narrow_road_cam.empty() || !segment.qcamera.empty()) streams.insert(VISION_STREAM_NARROW_ROAD); + if (replay->hasFlag(REPLAY_FLAG_CABIN_CAMERA) && !segment.cabin_cam.empty()) streams.insert(VISION_STREAM_CABIN); + if (replay->hasFlag(REPLAY_FLAG_WIDE_ROAD) && !segment.wide_road_cam.empty()) streams.insert(VISION_STREAM_WIDE_ROAD); + } + vipcAvailableStreamsUpdated(streams); + } } void VideoWidget::drawCameraWidget() { @@ -344,10 +354,10 @@ float VideoWidget::sizeHintHeight() const { return MIN_VIDEO_HEIGHT + SLIDER_HEIGHT + toolbarHeight(); } -// the video pane opens with the camera at its natural aspect ratio, filling the width of the dock +// Keep the pane's default proportions stable as frames arrive or cameras change. float VideoWidget::defaultHeight(float width) const { if (!cam_widget_) return toolbarHeight(); // live streams have no camera or slider - const float cam_height = std::max((float)MIN_VIDEO_HEIGHT, width / cam_widget_->frameAspectRatio()); + const float cam_height = std::max((float)MIN_VIDEO_HEIGHT, width / DEFAULT_CAMERA_ASPECT_RATIO); const float tab_height = camera_tab_->count() >= 2 ? ImGui::GetFrameHeight() : 0.0f; return cam_height + tab_height + SLIDER_HEIGHT + toolbarHeight(); }