app/ui.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // SPDX-License-Identifier: BSD-3-Clause | ||
| 2 | /// @file ui.cpp | ||
| 3 | /// @brief The viewer's UI, in the sections of ui.h: the panels, the theme and widgets, the orbit | ||
| 4 | /// camera, the shell and its menus, layout, mouse controls and heads-up display, and the | ||
| 5 | /// presenters: an offscreen image, and GLFW's windows, routed through volk's Vulkan loader, with | ||
| 6 | /// their swapchains and their input through ImGui's GLFW backend. | ||
| 7 | #include "ernie/app/ui.h" | ||
| 8 | |||
| 9 | #include "ernie/device/context.h" | ||
| 10 | #include "ernie/scene/scene.h" | ||
| 11 | #include "ernie/util/file.h" | ||
| 12 | #include "ernie/util/on_failure.h" | ||
| 13 | #include "ernie/util/profiler.h" | ||
| 14 | #include "ernie/util/text.h" | ||
| 15 | #include "ernie/util/version.h" | ||
| 16 | |||
| 17 | #include <glm/geometric.hpp> | ||
| 18 | #include <glm/gtc/matrix_transform.hpp> | ||
| 19 | #include <glm/gtc/type_ptr.hpp> | ||
| 20 | #include <imgui_internal.h> | ||
| 21 | |||
| 22 | #if ERNIE_VIEWER_WINDOW | ||
| 23 | #define GLFW_INCLUDE_NONE | ||
| 24 | #include <GLFW/glfw3.h> | ||
| 25 | #include <imgui_impl_glfw.h> | ||
| 26 | #endif | ||
| 27 | |||
| 28 | #include <array> | ||
| 29 | #include <cfloat> | ||
| 30 | #include <cmath> | ||
| 31 | #include <cstdio> | ||
| 32 | #include <filesystem> | ||
| 33 | #include <limits> | ||
| 34 | #include <span> | ||
| 35 | #include <stdexcept> | ||
| 36 | #include <type_traits> | ||
| 37 | #include <variant> | ||
| 38 | |||
| 39 | namespace ernie::app { | ||
| 40 | |||
| 41 | // Panels | ||
| 42 | |||
| 43 | ✗ | void MessageLog::Add(Severity severity, std::string text) { | |
| 44 | ✗ | const std::chrono::duration<double> elapsed = std::chrono::steady_clock::now() - start_; | |
| 45 | ✗ | messages_.push_back({elapsed.count(), severity, std::move(text)}); | |
| 46 | ✗ | while (messages_.size() > limit_) { | |
| 47 | ✗ | messages_.pop_front(); | |
| 48 | } | ||
| 49 | ✗ | } | |
| 50 | |||
| 51 | 10 | std::unordered_set<Path> StageMatches(const Scene& scene, std::string_view filter) { | |
| 52 | 10 | std::unordered_set<Path> matches; | |
| 53 | 10 | if (filter.empty()) { | |
| 54 | return matches; | ||
| 55 | } | ||
| 56 | 2 | const std::string wanted = ToLower(filter); | |
| 57 | 10 | for (const Path& path : scene.Prims()) { | |
| 58 | 16 | if (ToLower(path.String()).find(wanted) == std::string::npos) { | |
| 59 | 7 | continue; | |
| 60 | } | ||
| 61 | 7 | for (Path shown = path; !shown.IsEmpty() && !shown.IsRoot(); shown = shown.Parent()) { | |
| 62 | 3 | if (!matches.insert(shown).second) { | |
| 63 | break; | ||
| 64 | } | ||
| 65 | 1 | } | |
| 66 | 2 | } | |
| 67 | 2 | return matches; | |
| 68 | 2 | } | |
| 69 | |||
| 70 | namespace { | ||
| 71 | |||
| 72 | class LogPanel final : public Panel { | ||
| 73 | public: | ||
| 74 | 9 | LogPanel(MessageLog& log, const Theme& theme) | |
| 75 | 18 | : Panel("Log", Dock::Bottom), log_(log), theme_(theme) {} | |
| 76 | |||
| 77 | 19 | void Draw() override { | |
| 78 | 19 | ImGui::Checkbox("Info", &shown_[0]); | |
| 79 | 19 | ImGui::SameLine(); | |
| 80 | 19 | ImGui::Checkbox("Warnings", &shown_[1]); | |
| 81 | 19 | ImGui::SameLine(); | |
| 82 | 19 | ImGui::Checkbox("Errors", &shown_[2]); | |
| 83 | 19 | ImGui::SameLine(); | |
| 84 | 19 | ImGui::SetNextItemWidth(14.0f * ImGui::GetFontSize()); | |
| 85 | 19 | ImGui::InputTextWithHint("##filter", "Filter", filter_.data(), filter_.size()); | |
| 86 | 19 | ImGui::SameLine(); | |
| 87 | 19 | if (ImGui::Button("Clear")) { | |
| 88 | ✗ | log_.Clear(); | |
| 89 | } | ||
| 90 | 19 | ImGui::SameLine(); | |
| 91 | 19 | const bool copy = ImGui::Button("Copy"); | |
| 92 | 19 | std::string copied; | |
| 93 | 19 | if (ImGui::BeginChild("##messages", ImVec2(0.0f, 0.0f), ImGuiChildFlags_Borders)) { | |
| 94 | 19 | const std::string_view filter(filter_.data()); | |
| 95 | 19 | for (const Message& message : log_.Messages()) { | |
| 96 | ✗ | if (!shown_[static_cast<std::size_t>(message.severity)] || | |
| 97 | ✗ | (!filter.empty() && message.text.find(filter) == std::string::npos)) { | |
| 98 | ✗ | continue; | |
| 99 | } | ||
| 100 | ✗ | ImGui::TextDisabled("%8.2f", message.time); | |
| 101 | ✗ | ImGui::SameLine(); | |
| 102 | ✗ | ImGui::PushStyleColor(ImGuiCol_Text, Colour(message.severity)); | |
| 103 | ✗ | ImGui::TextWrapped("%s", message.text.c_str()); | |
| 104 | ✗ | ImGui::PopStyleColor(); | |
| 105 | ✗ | if (copy) { | |
| 106 | ✗ | copied += message.text + "\n"; | |
| 107 | } | ||
| 108 | } | ||
| 109 | // Follows new messages while scrolled to the end. | ||
| 110 | 19 | if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 1.0f) { | |
| 111 | 13 | ImGui::SetScrollHereY(1.0f); | |
| 112 | } | ||
| 113 | } | ||
| 114 | 19 | ImGui::EndChild(); | |
| 115 | 19 | if (copy) { | |
| 116 | ✗ | ImGui::SetClipboardText(copied.c_str()); | |
| 117 | } | ||
| 118 | 19 | } | |
| 119 | |||
| 120 | private: | ||
| 121 | ✗ | ImVec4 Colour(Severity severity) const { | |
| 122 | ✗ | switch (severity) { | |
| 123 | ✗ | case Severity::Warning: | |
| 124 | ✗ | return ToImVec4(theme_.warning); | |
| 125 | ✗ | case Severity::Error: | |
| 126 | ✗ | return ToImVec4(theme_.error); | |
| 127 | case Severity::Info: | ||
| 128 | break; | ||
| 129 | } | ||
| 130 | ✗ | return ImGui::GetStyleColorVec4(ImGuiCol_Text); | |
| 131 | } | ||
| 132 | |||
| 133 | MessageLog& log_; | ||
| 134 | const Theme& theme_; | ||
| 135 | std::array<bool, 3> shown_{true, true, true}; | ||
| 136 | std::array<char, 128> filter_{}; | ||
| 137 | }; | ||
| 138 | |||
| 139 | class StagePanel final : public Panel { | ||
| 140 | public: | ||
| 141 | 9 | StagePanel(const Scene& scene, Selection& selection) | |
| 142 | 18 | : Panel("Stage", Dock::Right), scene_(scene), selection_(selection) {} | |
| 143 | |||
| 144 | 20 | void Draw() override { | |
| 145 | 20 | ImGui::SetNextItemWidth(-1.0f); | |
| 146 | 20 | if (ImGui::InputTextWithHint("##search", "Search", search_.data(), search_.size()) || | |
| 147 | 20 | version_ != scene_.Version()) { | |
| 148 | 7 | matches_ = StageMatches(scene_, search_.data()); | |
| 149 | 7 | version_ = scene_.Version(); | |
| 150 | } | ||
| 151 | 20 | constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY | | |
| 152 | ImGuiTableFlags_Resizable | | ||
| 153 | ImGuiTableFlags_BordersInnerV; | ||
| 154 | 20 | if (!ImGui::BeginTable("##prims", 2, kFlags)) { | |
| 155 | return; | ||
| 156 | } | ||
| 157 | 20 | ImGui::TableSetupScrollFreeze(0, 1); | |
| 158 | 20 | ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); | |
| 159 | 20 | ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, | |
| 160 | 20 | 8.0f * ImGui::GetFontSize()); | |
| 161 | 20 | ImGui::TableHeadersRow(); | |
| 162 | 20 | DrawChildren(Path::Root()); | |
| 163 | 20 | ImGui::EndTable(); | |
| 164 | } | ||
| 165 | |||
| 166 | private: | ||
| 167 | 34 | bool Searching() const { | |
| 168 | 34 | return search_[0] != '\0'; | |
| 169 | } | ||
| 170 | |||
| 171 | 20 | void DrawChildren(const Path& parent) { | |
| 172 | 37 | for (const Path& child : scene_.Children(parent)) { | |
| 173 | 17 | if (!Searching() || matches_.contains(child)) { | |
| 174 | 17 | DrawPrim(child); | |
| 175 | } | ||
| 176 | 20 | } | |
| 177 | 20 | } | |
| 178 | |||
| 179 | 17 | void DrawPrim(const Path& path) { | |
| 180 | 17 | ImGui::TableNextRow(); | |
| 181 | 17 | ImGui::TableNextColumn(); | |
| 182 | 17 | const bool leaf = scene_.Children(path).empty(); | |
| 183 | 17 | ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_SpanAllColumns | | |
| 184 | ImGuiTreeNodeFlags_OpenOnArrow | | ||
| 185 | ImGuiTreeNodeFlags_OpenOnDoubleClick; | ||
| 186 | 17 | if (leaf) { | |
| 187 | 3 | flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; | |
| 188 | } | ||
| 189 | 17 | if (selection_.Contains(path)) { | |
| 190 | ✗ | flags |= ImGuiTreeNodeFlags_Selected; | |
| 191 | } | ||
| 192 | // Searching opens the matches' ancestors. | ||
| 193 | 17 | if (Searching()) { | |
| 194 | ✗ | ImGui::SetNextItemOpen(true); | |
| 195 | } | ||
| 196 | 17 | const std::string name(path.Name()); | |
| 197 | 17 | const bool open = ImGui::TreeNodeEx(path.String().c_str(), flags, "%s", name.c_str()); | |
| 198 | 17 | if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) { | |
| 199 | ✗ | if (ImGui::GetIO().KeyCtrl) { | |
| 200 | ✗ | selection_.Toggle(path); | |
| 201 | } else { | ||
| 202 | ✗ | selection_.Set(path); | |
| 203 | } | ||
| 204 | } | ||
| 205 | 17 | TableTexts({scene_.TypeName(scene_.Require(path))}); | |
| 206 | 17 | if (open && !leaf) { | |
| 207 | ✗ | DrawChildren(path); | |
| 208 | ✗ | ImGui::TreePop(); | |
| 209 | } | ||
| 210 | 17 | } | |
| 211 | |||
| 212 | const Scene& scene_; | ||
| 213 | Selection& selection_; | ||
| 214 | std::array<char, 128> search_{}; | ||
| 215 | std::unordered_set<Path> matches_; | ||
| 216 | std::uint64_t version_ = 0; | ||
| 217 | }; | ||
| 218 | |||
| 219 | /// @return The world box around the prims of `paths` the scene has. | ||
| 220 | 12 | Box WorldBounds(const Scene& scene, const std::vector<Path>& paths) { | |
| 221 | 12 | Box bounds; | |
| 222 | 22 | for (const Path& path : paths) { | |
| 223 | 10 | if (scene.Has(path)) { | |
| 224 | 10 | bounds.Extend(scene.WorldBounds(path)); | |
| 225 | } | ||
| 226 | } | ||
| 227 | 12 | return bounds; | |
| 228 | } | ||
| 229 | |||
| 230 | 23 | std::string VectorText(const glm::dvec3& vector) { | |
| 231 | 23 | std::array<char, 96> text{}; | |
| 232 | 23 | std::snprintf(text.data(), text.size(), "%.4g, %.4g, %.4g", vector.x, vector.y, vector.z); | |
| 233 | 23 | return text.data(); | |
| 234 | } | ||
| 235 | |||
| 236 | 9 | std::string BoxText(const Box& box) { | |
| 237 | 57 | return box.Empty() ? "empty" : VectorText(box.min) + " to " + VectorText(box.max) + " m"; | |
| 238 | } | ||
| 239 | |||
| 240 | 20 | std::string_view TypeLabel(std::string_view type) { | |
| 241 | 20 | return type.empty() ? "untyped" : type; | |
| 242 | } | ||
| 243 | |||
| 244 | ✗ | std::string ValueText(const Value& value) { | |
| 245 | ✗ | const auto numbers = [](const auto& components) { | |
| 246 | ✗ | return Join(components, ", ", [](auto n) { return ToText(static_cast<double>(n)); }); | |
| 247 | }; | ||
| 248 | ✗ | const auto vector = [&]<glm::length_t N, typename T>(const glm::vec<N, T>& v) { | |
| 249 | ✗ | return "(" + numbers(std::span(glm::value_ptr(v), static_cast<std::size_t>(N))) + ")"; | |
| 250 | }; | ||
| 251 | ✗ | return std::visit( | |
| 252 | ✗ | [&]<typename T>(const T& data) -> std::string { | |
| 253 | if constexpr (std::is_same_v<T, std::monostate>) { | ||
| 254 | return "none"; | ||
| 255 | } else if constexpr (std::is_same_v<T, bool>) { | ||
| 256 | return data ? "true" : "false"; | ||
| 257 | } else if constexpr (std::is_arithmetic_v<T>) { | ||
| 258 | return ToText(static_cast<double>(data)); | ||
| 259 | } else if constexpr (std::is_same_v<T, glm::dmat4>) { | ||
| 260 | ✗ | return Join(std::array{data[0], data[1], data[2], data[3]}, " ", vector); | |
| 261 | } else if constexpr (std::is_same_v<T, glm::dquat>) { | ||
| 262 | ✗ | return vector(glm::dvec4(data.w, data.x, data.y, data.z)); | |
| 263 | } else if constexpr (std::is_same_v<T, std::string>) { | ||
| 264 | return data; | ||
| 265 | } else if constexpr (std::is_same_v<T, Path>) { | ||
| 266 | return data.String(); | ||
| 267 | } else if constexpr (std::is_same_v<T, std::vector<float>>) { | ||
| 268 | ✗ | return "[" + numbers(data) + "]"; | |
| 269 | } else if constexpr (std::is_same_v<T, std::vector<Path>>) { | ||
| 270 | ✗ | return "[" + Join(data, ", ", &Path::String) + "]"; | |
| 271 | } else if constexpr (std::is_same_v<T, std::vector<std::string>>) { | ||
| 272 | ✗ | return "[" + Join(data, ", ") + "]"; | |
| 273 | } else { | ||
| 274 | return vector(data); | ||
| 275 | } | ||
| 276 | }, | ||
| 277 | ✗ | value.Raw()); | |
| 278 | } | ||
| 279 | |||
| 280 | ✗ | std::string AttributeText(const Scene& scene, PrimId prim, const std::string& attribute) { | |
| 281 | ✗ | const std::size_t samples = scene.Samples(prim, attribute).size(); | |
| 282 | ✗ | const std::string sampled = samples > 0 ? ", " + std::to_string(samples) + " samples" : ""; | |
| 283 | ✗ | if (const Tensor* array = scene.Read(prim, attribute)) { | |
| 284 | ✗ | return "array of " + Join(array->Shape(), " x ", [](auto n) { return std::to_string(n); }) + | |
| 285 | ✗ | ", " + FormatBytes(array->Bytes()) + (array->OnHost() ? "" : " on a device") + | |
| 286 | ✗ | sampled; | |
| 287 | } | ||
| 288 | ✗ | const std::optional<Value> value = scene.Get(prim, attribute); | |
| 289 | ✗ | return (value ? ValueText(*value) : "none") + sampled; | |
| 290 | ✗ | } | |
| 291 | |||
| 292 | /// @brief After Omniverse Kit's Property window, read-only: the first selected prim's path, type, | ||
| 293 | /// API schemas and bounds, then its authored attributes. | ||
| 294 | class PropertiesPanel final : public Panel { | ||
| 295 | public: | ||
| 296 | 9 | PropertiesPanel(const Scene& scene, const Selection& selection) | |
| 297 | 18 | : Panel("Properties", Dock::RightLower), scene_(scene), selection_(selection) {} | |
| 298 | |||
| 299 | 20 | void Draw() override { | |
| 300 | 20 | if (selection_.Empty() || !scene_.Has(selection_.Paths().front())) { | |
| 301 | 19 | ImGui::TextDisabled("Select a prim in the Stage."); | |
| 302 | 38 | return; | |
| 303 | } | ||
| 304 | 1 | const Path& path = selection_.Paths().front(); | |
| 305 | 1 | const PrimId prim = scene_.Require(path); | |
| 306 | 1 | ImGui::SetNextItemWidth(-1.0f); | |
| 307 | 1 | ImGui::InputTextWithHint("##search", "Search", search_.data(), search_.size()); | |
| 308 | 1 | LabelValue("Path", path.String()); | |
| 309 | 1 | LabelValue("Type", TypeLabel(scene_.TypeName(prim))); | |
| 310 | 1 | if (const std::vector<std::string> apis = scene_.AppliedApis(prim); !apis.empty()) { | |
| 311 | ✗ | LabelValue("API schemas", Join(apis, ", ")); | |
| 312 | 1 | } | |
| 313 | 1 | LabelValue("World bounds", BoxText(scene_.WorldBounds(path))); | |
| 314 | 1 | if (const std::size_t selected = selection_.Paths().size(); selected > 1) { | |
| 315 | ✗ | ImGui::TextDisabled("The first of %zu selected.", selected); | |
| 316 | } | ||
| 317 | 1 | constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY | | |
| 318 | ImGuiTableFlags_Resizable | | ||
| 319 | ImGuiTableFlags_BordersInnerV; | ||
| 320 | 1 | if (!ImGui::BeginTable("##attributes", 2, kFlags)) { | |
| 321 | return; | ||
| 322 | } | ||
| 323 | 1 | ImGui::TableSetupScrollFreeze(0, 1); | |
| 324 | 1 | ImGui::TableSetupColumn("Attribute"); | |
| 325 | 1 | ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); | |
| 326 | 1 | ImGui::TableHeadersRow(); | |
| 327 | 1 | const std::string wanted = ToLower(search_.data()); | |
| 328 | 1 | for (const std::string& attribute : scene_.Attributes(path)) { | |
| 329 | ✗ | if (ToLower(attribute).find(wanted) == std::string::npos) { | |
| 330 | ✗ | continue; | |
| 331 | } | ||
| 332 | ✗ | ImGui::TableNextRow(); | |
| 333 | ✗ | TableTexts({attribute, AttributeText(scene_, prim, attribute)}); | |
| 334 | 1 | } | |
| 335 | 1 | ImGui::EndTable(); | |
| 336 | 1 | } | |
| 337 | |||
| 338 | private: | ||
| 339 | const Scene& scene_; | ||
| 340 | const Selection& selection_; | ||
| 341 | std::array<char, 128> search_{}; | ||
| 342 | }; | ||
| 343 | |||
| 344 | /// @brief After Ernie 1's Info panel: the scene's scale and the device's use, in sections side by | ||
| 345 | /// side, as the bottom dock is wide and short. | ||
| 346 | class InfoPanel final : public Panel { | ||
| 347 | public: | ||
| 348 | 9 | InfoPanel(const Scene& scene, const DeviceContext* device) | |
| 349 | 18 | : Panel("Info", Dock::Bottom), scene_(scene), device_(device) {} | |
| 350 | |||
| 351 | 8 | void Draw() override { | |
| 352 | 8 | if (counted_ != scene_.Version()) { | |
| 353 | 8 | Count(); | |
| 354 | } | ||
| 355 | 8 | if (!ImGui::BeginTable("##info", 3, ImGuiTableFlags_SizingStretchSame)) { | |
| 356 | return; | ||
| 357 | } | ||
| 358 | 8 | ImGui::TableNextColumn(); | |
| 359 | 8 | ImGui::SeparatorText("Scene"); | |
| 360 | 8 | LabelValue("Prims", std::to_string(prims_)); | |
| 361 | 8 | LabelValue("Bounds", BoxText(bounds_)); | |
| 362 | 22 | LabelValue("Size", bounds_.Empty() ? "none" : VectorText(bounds_.max - bounds_.min) + " m"); | |
| 363 | 8 | LabelValue("Time code", ToText(scene_.Time())); | |
| 364 | 8 | ImGui::TableNextColumn(); | |
| 365 | 8 | ImGui::SeparatorText("Prims by type"); | |
| 366 | 27 | for (const auto& [type, count] : types_) { | |
| 367 | 38 | LabelValue(TypeLabel(type), std::to_string(count)); | |
| 368 | } | ||
| 369 | 8 | ImGui::TableNextColumn(); | |
| 370 | 8 | ImGui::SeparatorText("Device"); | |
| 371 | 8 | DrawDevice(); | |
| 372 | 8 | ImGui::EndTable(); | |
| 373 | } | ||
| 374 | |||
| 375 | private: | ||
| 376 | 8 | void Count() { | |
| 377 | 8 | prims_ = 0; | |
| 378 | 8 | types_.clear(); | |
| 379 | 33 | for (const Path& path : scene_.Prims()) { | |
| 380 | 25 | if (!path.IsRoot()) { | |
| 381 | 25 | ++prims_; | |
| 382 | 25 | ++types_[std::string(scene_.TypeName(scene_.Require(path)))]; | |
| 383 | } | ||
| 384 | 8 | } | |
| 385 | 8 | bounds_ = WorldBounds(scene_, scene_.Children(Path::Root())); | |
| 386 | 8 | counted_ = scene_.Version(); | |
| 387 | 8 | } | |
| 388 | |||
| 389 | 8 | void DrawDevice() const { | |
| 390 | 8 | const float fps = ImGui::GetIO().Framerate; | |
| 391 | 8 | std::array<char, 48> frame{}; | |
| 392 | 8 | std::snprintf(frame.data(), frame.size(), "%.2f ms, %.0f fps", | |
| 393 | fps > 0.0f ? 1000.0 / fps : 0.0, static_cast<double>(fps)); | ||
| 394 | 8 | LabelValue("Frame", frame.data()); | |
| 395 | 8 | if (device_ == nullptr) { | |
| 396 | 6 | ImGui::TextDisabled("No device in use."); | |
| 397 | 6 | return; | |
| 398 | } | ||
| 399 | 2 | const Transfers moved = device_->Transfers(); | |
| 400 | 2 | LabelValue("Uploaded", FormatBytes(moved.uploaded)); | |
| 401 | 2 | LabelValue("Read back", FormatBytes(moved.readBack)); | |
| 402 | 2 | LabelValue("Copied", FormatBytes(moved.copied)); | |
| 403 | 2 | const VkPhysicalDeviceMemoryProperties* memory = nullptr; | |
| 404 | 2 | vmaGetMemoryProperties(device_->Allocator(), &memory); | |
| 405 | 2 | std::array<VmaBudget, VK_MAX_MEMORY_HEAPS> budgets{}; | |
| 406 | 2 | vmaGetHeapBudgets(device_->Allocator(), budgets.data()); | |
| 407 | 4 | for (std::uint32_t heap = 0; heap < memory->memoryHeapCount; ++heap) { | |
| 408 | 2 | const VkMemoryHeapFlags flags = memory->memoryHeaps[heap].flags; | |
| 409 | 2 | const bool local = (flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0; | |
| 410 | 6 | LabelValue("Heap " + std::to_string(heap) + (local ? ", device" : ", host"), | |
| 411 | 6 | FormatBytes(budgets[heap].usage) + " of " + | |
| 412 | 4 | FormatBytes(budgets[heap].budget)); | |
| 413 | } | ||
| 414 | } | ||
| 415 | |||
| 416 | const Scene& scene_; | ||
| 417 | const DeviceContext* device_; | ||
| 418 | std::optional<std::uint64_t> counted_; | ||
| 419 | std::size_t prims_ = 0; | ||
| 420 | std::map<std::string, std::size_t> types_; | ||
| 421 | Box bounds_; | ||
| 422 | }; | ||
| 423 | |||
| 424 | 2 | std::string ApiVersion(const VulkanDeviceInfo& device) { | |
| 425 | 6 | return std::to_string(device.apiMajor) + "." + std::to_string(device.apiMinor); | |
| 426 | } | ||
| 427 | |||
| 428 | /// @return The capabilities a device has, as a list of names. | ||
| 429 | 2 | std::string CapabilityNames(const DeviceCapabilities& capabilities) { | |
| 430 | 2 | const std::pair<bool, const char*> all[] = { | |
| 431 | 2 | {capabilities.rayQuery, "ray queries"}, | |
| 432 | 2 | {capabilities.externalMemory, "shared memory"}, | |
| 433 | 2 | {capabilities.externalTimeline, "shared timelines"}, | |
| 434 | 2 | {capabilities.shaderInt64, "64-bit integers"}, | |
| 435 | 2 | {capabilities.shaderFloat16, "16-bit floats"}, | |
| 436 | 2 | {capabilities.memoryBudget, "memory budget"}, | |
| 437 | 2 | {capabilities.deviceFault, "fault reports"}, | |
| 438 | 2 | {capabilities.calibratedTimestamps, "GPU zones"}, | |
| 439 | 2 | {capabilities.present, "presenting"}, | |
| 440 | 2 | {capabilities.sparseResidency, "sparse residency"}, | |
| 441 | 2 | }; | |
| 442 | 2 | std::vector<std::string_view> names; | |
| 443 | 22 | for (const auto& [has, name] : all) { | |
| 444 | 20 | if (has) { | |
| 445 | 14 | names.emplace_back(name); | |
| 446 | } | ||
| 447 | } | ||
| 448 | 2 | return names.empty() ? "none" : Join(names, ", "); | |
| 449 | 2 | } | |
| 450 | |||
| 451 | class DevicePanel final : public Panel { | ||
| 452 | public: | ||
| 453 | 9 | explicit DevicePanel(const DeviceContext* device) | |
| 454 | 18 | : Panel("Devices", Dock::Bottom), device_(device) {} | |
| 455 | |||
| 456 | 8 | void Draw() override { | |
| 457 | 8 | if (device_ != nullptr) { | |
| 458 | 2 | const VulkanDeviceInfo& info = device_->Info(); | |
| 459 | 2 | ImGui::SeparatorText("In use"); | |
| 460 | 2 | LabelValue("Name", info.name); | |
| 461 | 2 | LabelValue("Type", ToString(info.type)); | |
| 462 | 2 | LabelValue("Vulkan", ApiVersion(info)); | |
| 463 | 2 | LabelValue("UUID", info.uuid); | |
| 464 | 2 | LabelValue("Capabilities", CapabilityNames(info.capabilities)); | |
| 465 | } else { | ||
| 466 | 6 | ImGui::TextDisabled("No device in use."); | |
| 467 | } | ||
| 468 | // Listed when first opened: listing makes a Vulkan instance. | ||
| 469 | 8 | if (!ImGui::CollapsingHeader("Installed")) { | |
| 470 | return; | ||
| 471 | } | ||
| 472 | ✗ | if (ImGui::Button("Refresh") || !installed_) { | |
| 473 | ✗ | List(); | |
| 474 | } | ||
| 475 | ✗ | if (!error_.empty()) { | |
| 476 | ✗ | ImGui::TextWrapped("%s", error_.c_str()); | |
| 477 | } | ||
| 478 | ✗ | DrawInstalled(); | |
| 479 | } | ||
| 480 | |||
| 481 | private: | ||
| 482 | ✗ | void List() { | |
| 483 | ✗ | try { | |
| 484 | ✗ | installed_ = ListVulkanDevices(); | |
| 485 | ✗ | error_.clear(); | |
| 486 | ✗ | } catch (const DeviceError& error) { | |
| 487 | ✗ | installed_ = std::vector<VulkanDeviceInfo>(); | |
| 488 | ✗ | error_ = error.what(); | |
| 489 | ✗ | } | |
| 490 | ✗ | } | |
| 491 | |||
| 492 | ✗ | void DrawInstalled() const { | |
| 493 | ✗ | constexpr ImGuiTableFlags kFlags = | |
| 494 | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_Resizable; | ||
| 495 | ✗ | if (!ImGui::BeginTable("##devices", 4, kFlags)) { | |
| 496 | return; | ||
| 497 | } | ||
| 498 | ✗ | ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); | |
| 499 | ✗ | ImGui::TableSetupColumn("Type"); | |
| 500 | ✗ | ImGui::TableSetupColumn("Vulkan"); | |
| 501 | ✗ | ImGui::TableSetupColumn("Supported"); | |
| 502 | ✗ | ImGui::TableHeadersRow(); | |
| 503 | ✗ | for (const VulkanDeviceInfo& device : *installed_) { | |
| 504 | ✗ | ImGui::TableNextRow(); | |
| 505 | ✗ | TableTexts({device.name}); | |
| 506 | ✗ | ImGui::SetItemTooltip("%s", device.uuid.c_str()); | |
| 507 | ✗ | TableTexts( | |
| 508 | ✗ | {ToString(device.type), ApiVersion(device), device.supported ? "yes" : "no"}); | |
| 509 | } | ||
| 510 | ✗ | ImGui::EndTable(); | |
| 511 | } | ||
| 512 | |||
| 513 | const DeviceContext* device_; | ||
| 514 | std::optional<std::vector<VulkanDeviceInfo>> installed_; | ||
| 515 | std::string error_; | ||
| 516 | }; | ||
| 517 | |||
| 518 | using profiler::Frame; | ||
| 519 | using profiler::ReportFilter; | ||
| 520 | using profiler::ReportNode; | ||
| 521 | using profiler::ReportRow; | ||
| 522 | |||
| 523 | ✗ | double FrameMs(const Frame& frame) { | |
| 524 | ✗ | return static_cast<double>(frame.end - frame.start) * 1e-6; | |
| 525 | } | ||
| 526 | |||
| 527 | /// @brief After Omniverse Kit's Profiler window: CPU and GPU zones averaged into trees, a bar per | ||
| 528 | /// frame, and exports as text, JSON and a Chrome trace. | ||
| 529 | class ProfilerPanel final : public Panel { | ||
| 530 | public: | ||
| 531 | 9 | ProfilerPanel(MessageLog& log, bool alwaysRecord) | |
| 532 | 18 | : Panel("Profiler", Dock::Bottom), log_(log), alwaysRecord_(alwaysRecord) {} | |
| 533 | |||
| 534 | 8 | void Draw() override { | |
| 535 | 8 | profiler::SetCpuEnabled(cpu_ || alwaysRecord_); | |
| 536 | 8 | profiler::SetGpuEnabled(gpu_ || alwaysRecord_); | |
| 537 | 8 | if (!paused_) { | |
| 538 | // Not the newest, whose GPU zones may still be arriving. | ||
| 539 | 8 | frames_ = profiler::Frames(); | |
| 540 | 8 | frames_.resize(frames_.size() - std::min(frames_.size(), profiler::kFramesInFlight)); | |
| 541 | } | ||
| 542 | 8 | DrawControls(); | |
| 543 | 8 | if (ImGui::BeginTabBar("##profiler")) { | |
| 544 | 8 | if (ImGui::BeginTabItem("Zones")) { | |
| 545 | 8 | DrawZones(); | |
| 546 | 8 | ImGui::EndTabItem(); | |
| 547 | } | ||
| 548 | 8 | if (ImGui::BeginTabItem("Frames")) { | |
| 549 | ✗ | DrawFrameBars(); | |
| 550 | ✗ | ImGui::EndTabItem(); | |
| 551 | } | ||
| 552 | 8 | ImGui::EndTabBar(); | |
| 553 | } | ||
| 554 | 8 | } | |
| 555 | |||
| 556 | /// Recording costs time, so it stops while nobody looks. | ||
| 557 | 12 | void Undrawn() override { | |
| 558 | 12 | if (!alwaysRecord_) { | |
| 559 | 12 | profiler::SetEnabled(false); | |
| 560 | } | ||
| 561 | 12 | } | |
| 562 | |||
| 563 | private: | ||
| 564 | 8 | void DrawControls() { | |
| 565 | 8 | const float number = 5.0f * ImGui::GetFontSize(); | |
| 566 | 8 | ImGui::Checkbox("CPU", &cpu_); | |
| 567 | 8 | ImGui::SetItemTooltip("Record the CPU's zones (ERNIE_ZONE)."); | |
| 568 | 8 | ImGui::SameLine(); | |
| 569 | 8 | ImGui::Checkbox("GPU", &gpu_); | |
| 570 | 8 | ImGui::SetItemTooltip("Record the GPU's zones (ERNIE_GPU_ZONE)."); | |
| 571 | 8 | ImGui::SameLine(); | |
| 572 | 8 | ImGui::Checkbox("Pause", &paused_); | |
| 573 | 8 | ImGui::SameLine(); | |
| 574 | 8 | if (ImGui::Button("Clear")) { | |
| 575 | ✗ | profiler::Clear(); | |
| 576 | ✗ | frames_.clear(); | |
| 577 | ✗ | selected_.reset(); | |
| 578 | } | ||
| 579 | 8 | if (!frames_.empty()) { | |
| 580 | ✗ | ImGui::SameLine(); | |
| 581 | ✗ | ImGui::TextDisabled("%zu frames, %.1f fps", frames_.size(), | |
| 582 | ✗ | 1000.0 / std::max(MeanMs(), 1e-3)); | |
| 583 | } | ||
| 584 | |||
| 585 | 8 | ImGui::Checkbox("Sort by time", &byTime_); | |
| 586 | 8 | ImGui::SameLine(); | |
| 587 | 8 | ImGui::SetNextItemWidth(number); | |
| 588 | 8 | ImGui::InputDouble("Min ms", &minMs_, 0.0, 0.0, "%.3f"); | |
| 589 | 8 | minMs_ = std::max(minMs_, 0.0); | |
| 590 | 8 | ImGui::SameLine(); | |
| 591 | 8 | ImGui::SetNextItemWidth(number); | |
| 592 | 8 | ImGui::InputInt("Depth", &depth_, 0, 0); | |
| 593 | 8 | depth_ = std::clamp(depth_, 0, 32); | |
| 594 | 8 | ImGui::SameLine(); | |
| 595 | 8 | ImGui::Checkbox("Average over (s)", &average_); | |
| 596 | 8 | ImGui::SetItemTooltip("Average the zones over the frames of the last seconds; else show " | |
| 597 | "the newest frame, or the one clicked in Frames."); | ||
| 598 | 8 | ImGui::SameLine(); | |
| 599 | 8 | ImGui::SetNextItemWidth(number); | |
| 600 | 8 | ImGui::BeginDisabled(!average_); | |
| 601 | 8 | ImGui::InputDouble("##averageSeconds", &averageSeconds_, 0.0, 0.0, "%.2f"); | |
| 602 | 8 | averageSeconds_ = std::clamp(averageSeconds_, 0.05, 10.0); | |
| 603 | 8 | ImGui::EndDisabled(); | |
| 604 | |||
| 605 | 8 | if (ImGui::Button("Copy Text")) { | |
| 606 | ✗ | ImGui::SetClipboardText(profiler::ReportText(report_, Filter()).c_str()); | |
| 607 | } | ||
| 608 | 8 | ImGui::SameLine(); | |
| 609 | 8 | if (ImGui::Button("Save JSON")) { | |
| 610 | ✗ | Save("ernie_profile.json", profiler::ReportJson(report_)); | |
| 611 | } | ||
| 612 | 8 | ImGui::SameLine(); | |
| 613 | 8 | if (ImGui::Button("Save Trace")) { | |
| 614 | ✗ | Save("ernie_trace.json", profiler::TraceJson(frames_, profiler::ThreadNames())); | |
| 615 | } | ||
| 616 | 8 | ImGui::SetItemTooltip("The frames kept, as a Chrome trace for Perfetto or " | |
| 617 | "chrome://tracing."); | ||
| 618 | 8 | } | |
| 619 | |||
| 620 | ✗ | void Save(const std::filesystem::path& file, const std::string& text) { | |
| 621 | ✗ | const std::filesystem::path path = std::filesystem::absolute(file); | |
| 622 | ✗ | try { | |
| 623 | ✗ | WriteTextFile(path, text); | |
| 624 | ✗ | log_.Info("Saved " + path.string()); | |
| 625 | ✗ | } catch (const std::exception& error) { | |
| 626 | ✗ | log_.Error(error.what()); | |
| 627 | ✗ | } | |
| 628 | ✗ | } | |
| 629 | |||
| 630 | 12 | ReportFilter Filter() const { | |
| 631 | ✗ | return {static_cast<std::uint32_t>(depth_), minMs_, byTime_}; | |
| 632 | } | ||
| 633 | |||
| 634 | ✗ | double MeanMs() const { | |
| 635 | ✗ | double total = 0.0; | |
| 636 | ✗ | for (const Frame& frame : frames_) { | |
| 637 | ✗ | total += FrameMs(frame); | |
| 638 | } | ||
| 639 | ✗ | return frames_.empty() ? 0.0 : total / static_cast<double>(frames_.size()); | |
| 640 | } | ||
| 641 | |||
| 642 | /// @return The frame inspected: the one clicked, else the newest with GPU zones, as the | ||
| 643 | /// device runs a frame's commands a frame or two after the host records them. | ||
| 644 | ✗ | const Frame* Inspected() { | |
| 645 | ✗ | if (selected_) { | |
| 646 | ✗ | const auto found = std::ranges::find(frames_, *selected_, &Frame::index); | |
| 647 | ✗ | if (found != frames_.end()) { | |
| 648 | ✗ | return &*found; | |
| 649 | } | ||
| 650 | ✗ | selected_.reset(); | |
| 651 | } | ||
| 652 | ✗ | const auto withGpu = std::ranges::find_if(frames_.rbegin(), frames_.rend(), | |
| 653 | ✗ | [](const Frame& f) { return !f.gpu.empty(); }); | |
| 654 | ✗ | if (withGpu != frames_.rend()) { | |
| 655 | ✗ | return &*withGpu; | |
| 656 | } | ||
| 657 | ✗ | return frames_.empty() ? nullptr : &frames_.back(); | |
| 658 | } | ||
| 659 | |||
| 660 | /// @return The frames the Zones tab averages: those of the last `averageSeconds_`, else the | ||
| 661 | /// one inspected. | ||
| 662 | 8 | std::vector<Frame> Averaged() { | |
| 663 | 8 | if (!average_) { | |
| 664 | ✗ | const Frame* frame = Inspected(); | |
| 665 | ✗ | return frame != nullptr ? std::vector<Frame>{*frame} : std::vector<Frame>{}; | |
| 666 | } | ||
| 667 | 8 | std::vector<Frame> frames; | |
| 668 | 8 | if (frames_.empty()) { | |
| 669 | 8 | return frames; | |
| 670 | } | ||
| 671 | ✗ | const auto window = static_cast<std::uint64_t>(averageSeconds_ * 1e9); | |
| 672 | ✗ | for (const Frame& frame : frames_) { | |
| 673 | ✗ | if (frame.end + window >= frames_.back().end) { | |
| 674 | ✗ | frames.push_back(frame); | |
| 675 | } | ||
| 676 | } | ||
| 677 | ✗ | return frames; | |
| 678 | 8 | } | |
| 679 | |||
| 680 | 8 | void DrawZones() { | |
| 681 | 16 | report_ = profiler::Summarize(Averaged(), profiler::ThreadNames()); | |
| 682 | 8 | constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | | |
| 683 | ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable; | ||
| 684 | 8 | if (!ImGui::BeginTable("##zones", 4, kFlags)) { | |
| 685 | 2 | return; | |
| 686 | } | ||
| 687 | 6 | const float em = ImGui::GetFontSize(); | |
| 688 | 6 | ImGui::TableSetupScrollFreeze(0, 1); | |
| 689 | 6 | ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch); | |
| 690 | 6 | ImGui::TableSetupColumn("Duration (ms)", ImGuiTableColumnFlags_WidthFixed, 7.0f * em); | |
| 691 | 6 | ImGui::TableSetupColumn("Calls", ImGuiTableColumnFlags_WidthFixed, 3.5f * em); | |
| 692 | 6 | ImGui::TableSetupColumn("Percentage", ImGuiTableColumnFlags_WidthFixed, 8.0f * em); | |
| 693 | 6 | ImGui::TableHeadersRow(); | |
| 694 | 6 | DrawSection("GPU", report_.gpu, report_.gpuMs, report_.gpuFrames, Filter()); | |
| 695 | // The CPU's zones sit under a node per thread. | ||
| 696 | 6 | ReportFilter cpu = Filter(); | |
| 697 | 6 | ++cpu.depth; | |
| 698 | 6 | DrawSection("CPU", report_.cpu, report_.cpuMs, report_.cpuFrames, cpu); | |
| 699 | 6 | ImGui::EndTable(); | |
| 700 | } | ||
| 701 | |||
| 702 | /// One section of the Zones table: its total heading its nested zones. | ||
| 703 | 12 | void DrawSection(const char* title, const std::vector<ReportNode>& nodes, double totalMs, | |
| 704 | std::size_t frames, const ReportFilter& filter) { | ||
| 705 | 12 | ImGui::TableNextRow(); | |
| 706 | 12 | ImGui::TableNextColumn(); | |
| 707 | 12 | ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextLink)); | |
| 708 | 12 | const bool shown = ImGui::TreeNodeEx(title, ImGuiTreeNodeFlags_SpanAllColumns | | |
| 709 | ImGuiTreeNodeFlags_DefaultOpen); | ||
| 710 | 12 | ImGui::TableNextColumn(); | |
| 711 | 12 | ImGui::Text("%.3f", totalMs); | |
| 712 | 12 | ImGui::TableNextColumn(); | |
| 713 | 12 | ImGui::TableNextColumn(); | |
| 714 | 12 | if (frames == 0) { | |
| 715 | 12 | ImGui::TextUnformatted("(no frames)"); | |
| 716 | } else { | ||
| 717 | ✗ | ImGui::Text("%zu frame%s", frames, frames == 1 ? "" : "s"); | |
| 718 | } | ||
| 719 | 12 | ImGui::PopStyleColor(); | |
| 720 | 12 | if (!shown) { | |
| 721 | return; | ||
| 722 | } | ||
| 723 | 12 | DrawRows(profiler::Rows(nodes, filter)); | |
| 724 | 12 | ImGui::TreePop(); | |
| 725 | } | ||
| 726 | |||
| 727 | 12 | static void DrawRows(const std::vector<ReportRow>& rows) { | |
| 728 | 12 | constexpr std::uint32_t kNone = std::numeric_limits<std::uint32_t>::max(); | |
| 729 | 12 | std::uint32_t open = 0; // tree levels pushed | |
| 730 | 12 | std::uint32_t closed = kNone; // the depth of a closed parent | |
| 731 | 12 | for (const ReportRow& row : rows) { | |
| 732 | ✗ | if (row.depth > closed) { | |
| 733 | ✗ | continue; | |
| 734 | } | ||
| 735 | ✗ | closed = kNone; | |
| 736 | ✗ | for (; open > row.depth; --open) { | |
| 737 | ✗ | ImGui::TreePop(); | |
| 738 | } | ||
| 739 | ✗ | ImGui::TableNextRow(); | |
| 740 | ✗ | ImGui::TableNextColumn(); | |
| 741 | ✗ | ImGuiTreeNodeFlags flags = | |
| 742 | ImGuiTreeNodeFlags_SpanAllColumns | ImGuiTreeNodeFlags_DefaultOpen; | ||
| 743 | ✗ | if (!row.parent) { | |
| 744 | ✗ | flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; | |
| 745 | } | ||
| 746 | ✗ | const bool expanded = ImGui::TreeNodeEx(row.node->name.c_str(), flags); | |
| 747 | ✗ | if (row.parent) { | |
| 748 | ✗ | if (expanded) { | |
| 749 | ✗ | ++open; | |
| 750 | } else { | ||
| 751 | ✗ | closed = row.depth; | |
| 752 | } | ||
| 753 | } | ||
| 754 | ✗ | ImGui::TableNextColumn(); | |
| 755 | ✗ | ImGui::Text("%.3f", row.node->ms); | |
| 756 | ✗ | ImGui::TableNextColumn(); | |
| 757 | ✗ | ImGui::Text("%.3g", row.node->calls); | |
| 758 | ✗ | ImGui::TableNextColumn(); | |
| 759 | ✗ | std::array<char, 16> share{}; | |
| 760 | ✗ | std::snprintf(share.data(), share.size(), "%.2f%%", row.node->percent); | |
| 761 | ✗ | ImGui::ProgressBar(static_cast<float>(row.node->percent / 100.0), | |
| 762 | ✗ | ImVec2(-1.0f, ImGui::GetTextLineHeight()), share.data()); | |
| 763 | } | ||
| 764 | 12 | for (; open > 0; --open) { | |
| 765 | ✗ | ImGui::TreePop(); | |
| 766 | } | ||
| 767 | 12 | } | |
| 768 | |||
| 769 | /// A bar per frame, scaled to the frames' 95th percentile so one slow frame does not flatten | ||
| 770 | /// the rest; clicking one inspects it. | ||
| 771 | ✗ | void DrawFrameBars() { | |
| 772 | ✗ | const float height = 4.5f * ImGui::GetFontSize(); | |
| 773 | ✗ | const ImVec2 origin = ImGui::GetCursorScreenPos(); | |
| 774 | ✗ | const float width = std::max(ImGui::GetContentRegionAvail().x, 1.0f); | |
| 775 | ✗ | ImGui::InvisibleButton("##frames", ImVec2(width, height)); | |
| 776 | ✗ | const bool hovered = ImGui::IsItemHovered(); | |
| 777 | ✗ | ImDrawList* draw = ImGui::GetWindowDrawList(); | |
| 778 | ✗ | draw->AddRectFilled(origin, ImVec2(origin.x + width, origin.y + height), | |
| 779 | ImGui::GetColorU32(ImGuiCol_FrameBg)); | ||
| 780 | ✗ | if (frames_.empty()) { | |
| 781 | ✗ | ImGui::TextDisabled("No frames yet."); | |
| 782 | ✗ | return; | |
| 783 | } | ||
| 784 | ✗ | std::vector<double> times; | |
| 785 | ✗ | for (const Frame& frame : frames_) { | |
| 786 | ✗ | times.push_back(FrameMs(frame)); | |
| 787 | } | ||
| 788 | ✗ | std::ranges::sort(times); | |
| 789 | ✗ | const double longest = std::max(1000.0 / 30.0 * 1.1, 1.5 * times[times.size() * 95 / 100]); | |
| 790 | ✗ | const auto barHeight = [&](double ms) { | |
| 791 | ✗ | return static_cast<float>(std::min(ms / longest, 1.0)) * height; | |
| 792 | ✗ | }; | |
| 793 | // Guides at 60 and 30 frames per second. | ||
| 794 | ✗ | for (const double guide : {1000.0 / 60.0, 1000.0 / 30.0}) { | |
| 795 | ✗ | const float y = origin.y + height - barHeight(guide); | |
| 796 | ✗ | draw->AddLine(ImVec2(origin.x, y), ImVec2(origin.x + width, y), | |
| 797 | ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.5f)); | ||
| 798 | } | ||
| 799 | ✗ | const std::size_t slots = std::max(profiler::History(), frames_.size()); | |
| 800 | ✗ | const float bar = width / static_cast<float>(slots); | |
| 801 | ✗ | const float first = origin.x + width - bar * static_cast<float>(frames_.size()); | |
| 802 | ✗ | const Frame* inspected = Inspected(); | |
| 803 | ✗ | const ImVec2 mouse = ImGui::GetIO().MousePos; | |
| 804 | ✗ | for (std::size_t i = 0; i < frames_.size(); ++i) { | |
| 805 | ✗ | const Frame& frame = frames_[i]; | |
| 806 | ✗ | const double ms = FrameMs(frame); | |
| 807 | ✗ | const float x = first + bar * static_cast<float>(i); | |
| 808 | ✗ | const bool under = hovered && mouse.x >= x && mouse.x < x + bar; | |
| 809 | ✗ | const ImGuiCol colour = inspected != nullptr && frame.index == inspected->index | |
| 810 | ✗ | ? ImGuiCol_PlotHistogramHovered | |
| 811 | : (under ? ImGuiCol_ButtonHovered : ImGuiCol_PlotHistogram); | ||
| 812 | ✗ | draw->AddRectFilled(ImVec2(x, origin.y + height - barHeight(ms)), | |
| 813 | ✗ | ImVec2(x + std::max(bar - 1.0f, 1.0f), origin.y + height), | |
| 814 | ImGui::GetColorU32(colour)); | ||
| 815 | ✗ | if (under) { | |
| 816 | ✗ | ImGui::SetTooltip("Frame %llu: %.2f ms", | |
| 817 | ✗ | static_cast<unsigned long long>(frame.index), ms); | |
| 818 | ✗ | if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { | |
| 819 | ✗ | selected_ = frame.index; | |
| 820 | ✗ | paused_ = true; | |
| 821 | } | ||
| 822 | } | ||
| 823 | } | ||
| 824 | ✗ | if (inspected != nullptr) { | |
| 825 | ✗ | ImGui::Text("Frame %llu: %.2f ms, %zu CPU zones, %zu GPU zones", | |
| 826 | ✗ | static_cast<unsigned long long>(inspected->index), FrameMs(*inspected), | |
| 827 | inspected->cpu.size(), inspected->gpu.size()); | ||
| 828 | } | ||
| 829 | ✗ | } | |
| 830 | |||
| 831 | MessageLog& log_; | ||
| 832 | bool alwaysRecord_; | ||
| 833 | bool cpu_ = true; | ||
| 834 | bool gpu_ = true; | ||
| 835 | bool paused_ = false; | ||
| 836 | bool byTime_ = false; | ||
| 837 | double minMs_ = 0.0; | ||
| 838 | int depth_ = 8; | ||
| 839 | bool average_ = true; | ||
| 840 | double averageSeconds_ = 1.0; | ||
| 841 | std::vector<Frame> frames_; | ||
| 842 | std::optional<std::uint64_t> selected_; | ||
| 843 | profiler::Report report_; | ||
| 844 | }; | ||
| 845 | |||
| 846 | /// @brief A help window, centred when it opens, drawn while `open`; its close button clears it. | ||
| 847 | 40 | template <typename Contents> void HelpWindow(const char* title, bool& open, Contents&& contents) { | |
| 848 | 40 | if (!open) { | |
| 849 | return; | ||
| 850 | } | ||
| 851 | ✗ | ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, | |
| 852 | ✗ | ImVec2(0.5f, 0.5f)); | |
| 853 | ✗ | constexpr ImGuiWindowFlags kFlags = ImGuiWindowFlags_NoDocking | | |
| 854 | ImGuiWindowFlags_AlwaysAutoResize | | ||
| 855 | ImGuiWindowFlags_NoCollapse; | ||
| 856 | ✗ | if (ImGui::Begin(title, &open, kFlags)) { | |
| 857 | ✗ | contents(); | |
| 858 | } | ||
| 859 | ✗ | ImGui::End(); | |
| 860 | } | ||
| 861 | |||
| 862 | } // namespace | ||
| 863 | |||
| 864 | 9 | std::unique_ptr<Panel> MakeLogPanel(MessageLog& log, const Theme& theme) { | |
| 865 | 9 | return std::make_unique<LogPanel>(log, theme); | |
| 866 | } | ||
| 867 | |||
| 868 | 9 | std::unique_ptr<Panel> MakeStagePanel(const Scene& scene, Selection& selection) { | |
| 869 | 9 | return std::make_unique<StagePanel>(scene, selection); | |
| 870 | } | ||
| 871 | |||
| 872 | 9 | std::unique_ptr<Panel> MakePropertiesPanel(const Scene& scene, const Selection& selection) { | |
| 873 | 9 | return std::make_unique<PropertiesPanel>(scene, selection); | |
| 874 | } | ||
| 875 | |||
| 876 | 9 | std::unique_ptr<Panel> MakeInfoPanel(const Scene& scene, const DeviceContext* device) { | |
| 877 | 9 | return std::make_unique<InfoPanel>(scene, device); | |
| 878 | } | ||
| 879 | |||
| 880 | 9 | std::unique_ptr<Panel> MakeDevicePanel(const DeviceContext* device) { | |
| 881 | 9 | return std::make_unique<DevicePanel>(device); | |
| 882 | } | ||
| 883 | |||
| 884 | 9 | std::unique_ptr<Panel> MakeProfilerPanel(MessageLog& log, bool alwaysRecord) { | |
| 885 | 9 | return std::make_unique<ProfilerPanel>(log, alwaysRecord); | |
| 886 | } | ||
| 887 | |||
| 888 | 20 | void DrawControlsWindow(bool& open) { | |
| 889 | 20 | constexpr std::pair<const char*, const char*> kControls[] = { | |
| 890 | {"Left drag", "Orbit the camera around its target"}, | ||
| 891 | {"Right drag", "Pan along the ground"}, | ||
| 892 | {"Middle drag", "Pan across the view"}, | ||
| 893 | {"Wheel", "Zoom towards the target"}, | ||
| 894 | {"F", "Frame the selection, or the whole scene"}, | ||
| 895 | {"Click in Stage", "Select a prim; Ctrl adds or removes it"}, | ||
| 896 | {"Ctrl+Q", "Quit"}, | ||
| 897 | }; | ||
| 898 | 20 | HelpWindow("Keyboard and Mouse", open, [&] { | |
| 899 | ✗ | if (ImGui::BeginTable("##controls", 2, | |
| 900 | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV)) { | ||
| 901 | ✗ | for (const auto& [input, action] : kControls) { | |
| 902 | ✗ | ImGui::TableNextRow(); | |
| 903 | ✗ | TableTexts({input, action}); | |
| 904 | } | ||
| 905 | ✗ | ImGui::EndTable(); | |
| 906 | } | ||
| 907 | ✗ | }); | |
| 908 | 20 | } | |
| 909 | |||
| 910 | 20 | void DrawAboutWindow(bool& open, std::string_view device) { | |
| 911 | 20 | HelpWindow("About Ernie", open, [&] { | |
| 912 | ✗ | ImGui::TextUnformatted( | |
| 913 | "Ernie: Efficient Renderer for Natural and Interplanetary Environments"); | ||
| 914 | ✗ | LabelValue("Version", Version()); | |
| 915 | ✗ | LabelValue("Device", device.empty() ? std::string_view("none") : device); | |
| 916 | ✗ | LabelValue("Dear ImGui", IMGUI_VERSION); | |
| 917 | ✗ | ImGui::Separator(); | |
| 918 | ✗ | ImGui::TextUnformatted("BSD-3-Clause licence. https://github.com/alexmaietta/ernie"); | |
| 919 | ✗ | }); | |
| 920 | 20 | } | |
| 921 | |||
| 922 | // Theme and widgets | ||
| 923 | |||
| 924 | namespace { | ||
| 925 | |||
| 926 | 16 | ImVec2 ToImVec2(const glm::vec2& size) { | |
| 927 | 16 | return {size.x, size.y}; | |
| 928 | } | ||
| 929 | |||
| 930 | 56 | glm::vec4 WithAlpha(const glm::vec4& colour, float alpha) { | |
| 931 | 56 | return {colour.r, colour.g, colour.b, alpha}; | |
| 932 | } | ||
| 933 | |||
| 934 | 8 | void SetMetrics(const Theme& theme, ImGuiStyle& style) { | |
| 935 | 8 | style.WindowPadding = {8.0f, 6.0f}; | |
| 936 | 8 | style.FramePadding = ToImVec2(theme.framePadding); | |
| 937 | 8 | style.ItemSpacing = ToImVec2(theme.itemSpacing); | |
| 938 | 8 | style.ItemInnerSpacing = {4.0f, 3.0f}; | |
| 939 | 8 | style.CellPadding = {4.0f, 2.0f}; | |
| 940 | 8 | style.IndentSpacing = 14.0f; | |
| 941 | 8 | style.ScrollbarSize = 10.0f; | |
| 942 | 8 | style.GrabMinSize = 8.0f; | |
| 943 | 8 | style.WindowRounding = 0.0f; | |
| 944 | 8 | style.ChildRounding = 0.0f; | |
| 945 | 8 | style.FrameRounding = theme.rounding; | |
| 946 | 8 | style.PopupRounding = theme.rounding; | |
| 947 | 8 | style.ScrollbarRounding = theme.rounding; | |
| 948 | 8 | style.GrabRounding = theme.rounding; | |
| 949 | 8 | style.TabRounding = theme.rounding; | |
| 950 | 8 | style.WindowBorderSize = 1.0f; | |
| 951 | 8 | style.ChildBorderSize = 1.0f; | |
| 952 | 8 | style.PopupBorderSize = 1.0f; | |
| 953 | 8 | style.FrameBorderSize = theme.frameBorder; | |
| 954 | // Framed tabs, without a line over the shown one. | ||
| 955 | 8 | style.TabBorderSize = theme.frameBorder; | |
| 956 | 8 | style.TabBarOverlineSize = 0.0f; | |
| 957 | 8 | style.DockingSeparatorSize = 1.0f; | |
| 958 | 8 | style.WindowMenuButtonPosition = ImGuiDir_None; | |
| 959 | 8 | style.WindowTitleAlign = {0.0f, 0.5f}; | |
| 960 | 8 | style.SeparatorTextBorderSize = 1.0f; | |
| 961 | 8 | } | |
| 962 | |||
| 963 | 8 | void SetColours(const Theme& theme, ImGuiStyle& style) { | |
| 964 | 512 | const auto set = [&](ImGuiCol index, const glm::vec4& colour) { | |
| 965 | 504 | style.Colors[index] = ToImVec4(colour); | |
| 966 | 512 | }; | |
| 967 | 8 | const glm::vec4 accentDim = WithAlpha(theme.accent, 0.45f); | |
| 968 | 8 | const glm::vec4 accentFaint = WithAlpha(theme.accent, 0.25f); | |
| 969 | 8 | const glm::vec4 clear(0.0f); | |
| 970 | 8 | set(ImGuiCol_Text, theme.text); | |
| 971 | 8 | set(ImGuiCol_TextDisabled, theme.textMuted); | |
| 972 | 8 | set(ImGuiCol_WindowBg, theme.panel); | |
| 973 | 8 | set(ImGuiCol_ChildBg, clear); | |
| 974 | 8 | set(ImGuiCol_PopupBg, theme.popup); | |
| 975 | 8 | set(ImGuiCol_Border, theme.border); | |
| 976 | 8 | set(ImGuiCol_BorderShadow, clear); | |
| 977 | 8 | set(ImGuiCol_FrameBg, theme.field); | |
| 978 | 8 | set(ImGuiCol_FrameBgHovered, theme.fieldHovered); | |
| 979 | 8 | set(ImGuiCol_FrameBgActive, theme.fieldHovered); | |
| 980 | 32 | for (const ImGuiCol title : {ImGuiCol_TitleBg, ImGuiCol_TitleBgActive, | |
| 981 | 40 | ImGuiCol_TitleBgCollapsed, ImGuiCol_MenuBarBg}) { | |
| 982 | 32 | set(title, theme.background); | |
| 983 | } | ||
| 984 | 8 | set(ImGuiCol_ScrollbarBg, clear); | |
| 985 | 8 | set(ImGuiCol_ScrollbarGrab, theme.fieldHovered); | |
| 986 | 8 | set(ImGuiCol_ScrollbarGrabHovered, theme.textMuted); | |
| 987 | 8 | set(ImGuiCol_ScrollbarGrabActive, theme.accent); | |
| 988 | // A white box with a black check; menus mark their items in the accent instead. | ||
| 989 | 8 | set(ImGuiCol_CheckMark, theme.checkMark); | |
| 990 | 8 | set(ImGuiCol_CheckboxSelectedBg, theme.checkBox); | |
| 991 | 8 | set(ImGuiCol_SliderGrab, theme.buttonLit); | |
| 992 | 8 | set(ImGuiCol_SliderGrabActive, theme.text); | |
| 993 | 8 | set(ImGuiCol_Button, theme.button); | |
| 994 | 8 | set(ImGuiCol_ButtonHovered, theme.fieldHovered); | |
| 995 | 8 | set(ImGuiCol_ButtonActive, theme.buttonLit); | |
| 996 | // Selected and hovered rows of trees, lists and menus. | ||
| 997 | 8 | set(ImGuiCol_Header, theme.selection); | |
| 998 | 8 | set(ImGuiCol_HeaderHovered, WithAlpha(theme.selection, 0.85f)); | |
| 999 | 8 | set(ImGuiCol_HeaderActive, theme.selection); | |
| 1000 | 8 | set(ImGuiCol_Separator, theme.separator); | |
| 1001 | 8 | set(ImGuiCol_SeparatorHovered, accentDim); | |
| 1002 | 8 | set(ImGuiCol_SeparatorActive, theme.accent); | |
| 1003 | 8 | set(ImGuiCol_ResizeGrip, clear); | |
| 1004 | 8 | set(ImGuiCol_ResizeGripHovered, accentDim); | |
| 1005 | 8 | set(ImGuiCol_ResizeGripActive, theme.accent); | |
| 1006 | 8 | set(ImGuiCol_InputTextCursor, theme.text); | |
| 1007 | // Tabs on the darker bar, the shown one in the panel's colour. | ||
| 1008 | 8 | set(ImGuiCol_Tab, theme.background); | |
| 1009 | 8 | set(ImGuiCol_TabHovered, theme.fieldHovered); | |
| 1010 | 8 | set(ImGuiCol_TabSelected, theme.panel); | |
| 1011 | 8 | set(ImGuiCol_TabSelectedOverline, theme.accent); | |
| 1012 | 8 | set(ImGuiCol_TabDimmed, theme.background); | |
| 1013 | 8 | set(ImGuiCol_TabDimmedSelected, theme.panel); | |
| 1014 | 8 | set(ImGuiCol_TabDimmedSelectedOverline, WithAlpha(theme.accent, 0.0f)); | |
| 1015 | 8 | set(ImGuiCol_DockingPreview, accentDim); | |
| 1016 | 8 | set(ImGuiCol_DockingEmptyBg, theme.background); | |
| 1017 | 8 | set(ImGuiCol_PlotLines, theme.accent); | |
| 1018 | 8 | set(ImGuiCol_PlotLinesHovered, theme.text); | |
| 1019 | 8 | set(ImGuiCol_PlotHistogram, theme.accent); | |
| 1020 | 8 | set(ImGuiCol_PlotHistogramHovered, theme.text); | |
| 1021 | 8 | set(ImGuiCol_TableHeaderBg, theme.header); | |
| 1022 | 8 | set(ImGuiCol_TableBorderStrong, theme.separator); | |
| 1023 | 8 | set(ImGuiCol_TableBorderLight, theme.separator); | |
| 1024 | 8 | set(ImGuiCol_TableRowBg, clear); | |
| 1025 | 8 | set(ImGuiCol_TableRowBgAlt, WithAlpha(theme.text, 0.03f)); | |
| 1026 | 8 | set(ImGuiCol_TextLink, theme.accent); | |
| 1027 | 8 | set(ImGuiCol_TextSelectedBg, accentFaint); | |
| 1028 | 8 | set(ImGuiCol_TreeLines, theme.separator); | |
| 1029 | 8 | set(ImGuiCol_DragDropTarget, theme.accent); | |
| 1030 | 8 | set(ImGuiCol_DragDropTargetBg, accentFaint); | |
| 1031 | 8 | set(ImGuiCol_UnsavedMarker, theme.warning); | |
| 1032 | 8 | set(ImGuiCol_NavCursor, theme.accent); | |
| 1033 | 8 | set(ImGuiCol_NavWindowingHighlight, theme.accent); | |
| 1034 | 8 | set(ImGuiCol_NavWindowingDimBg, WithAlpha(theme.background, 0.6f)); | |
| 1035 | 8 | set(ImGuiCol_ModalWindowDimBg, WithAlpha(theme.background, 0.6f)); | |
| 1036 | 8 | } | |
| 1037 | |||
| 1038 | } // namespace | ||
| 1039 | |||
| 1040 | 8 | void ApplyTheme(const Theme& theme, float contentScale) { | |
| 1041 | 8 | ImGuiStyle& style = ImGui::GetStyle(); | |
| 1042 | 8 | style = ImGuiStyle(); | |
| 1043 | 8 | SetMetrics(theme, style); | |
| 1044 | 8 | SetColours(theme, style); | |
| 1045 | 8 | style.ScaleAllSizes(contentScale); | |
| 1046 | 8 | style.FontSizeBase = theme.fontSize; | |
| 1047 | 8 | style.FontScaleDpi = contentScale; | |
| 1048 | 8 | } | |
| 1049 | |||
| 1050 | 80 | void LabelValue(std::string_view label, std::string_view value) { | |
| 1051 | 80 | ImGui::TextUnformatted(label.data(), label.data() + label.size()); | |
| 1052 | 80 | ImGui::SameLine(); | |
| 1053 | 80 | ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); | |
| 1054 | 80 | ImGui::PushTextWrapPos(0.0f); | |
| 1055 | 80 | ImGui::TextUnformatted(value.data(), value.data() + value.size()); | |
| 1056 | 80 | ImGui::PopTextWrapPos(); | |
| 1057 | 80 | ImGui::PopStyleColor(); | |
| 1058 | 80 | } | |
| 1059 | |||
| 1060 | 17 | void TableTexts(std::initializer_list<std::string_view> texts) { | |
| 1061 | 34 | for (const std::string_view text : texts) { | |
| 1062 | 17 | ImGui::TableNextColumn(); | |
| 1063 | 17 | ImGui::TextUnformatted(text.data(), text.data() + text.size()); | |
| 1064 | } | ||
| 1065 | 17 | } | |
| 1066 | |||
| 1067 | 10 | std::string FormatBytes(std::uint64_t bytes) { | |
| 1068 | 10 | constexpr std::array kUnits{"B", "KiB", "MiB", "GiB", "TiB"}; | |
| 1069 | 10 | auto value = static_cast<double>(bytes); | |
| 1070 | 10 | std::size_t unit = 0; | |
| 1071 | 22 | while (value >= 1024.0 && unit + 1 < kUnits.size()) { | |
| 1072 | 12 | value /= 1024.0; | |
| 1073 | 12 | ++unit; | |
| 1074 | } | ||
| 1075 | 10 | std::array<char, 32> text{}; | |
| 1076 | 14 | std::snprintf(text.data(), text.size(), unit == 0 ? "%.0f %s" : "%.1f %s", value, kUnits[unit]); | |
| 1077 | 10 | return text.data(); | |
| 1078 | } | ||
| 1079 | |||
| 1080 | // Orbit camera | ||
| 1081 | |||
| 1082 | namespace { | ||
| 1083 | |||
| 1084 | /// @return Metres per pixel at the target's depth in a view `height` pixels tall. | ||
| 1085 | 2 | double MetresPerPixel(const OrbitCamera& camera, double height) { | |
| 1086 | 2 | return 2.0 * camera.distance * std::tan(0.5 * camera.verticalFov) / std::max(height, 1.0); | |
| 1087 | } | ||
| 1088 | |||
| 1089 | } // namespace | ||
| 1090 | |||
| 1091 | 24 | glm::dvec3 OrbitCamera::Eye() const { | |
| 1092 | 24 | const double horizontal = std::cos(pitch) * distance; | |
| 1093 | 48 | return target + glm::dvec3(std::sin(yaw) * horizontal, -std::cos(yaw) * horizontal, | |
| 1094 | 24 | std::sin(pitch) * distance); | |
| 1095 | } | ||
| 1096 | |||
| 1097 | 7 | glm::dmat4 OrbitCamera::Transform() const { | |
| 1098 | 7 | return glm::inverse(glm::lookAtRH(Eye(), target, glm::dvec3(0.0, 0.0, 1.0))); | |
| 1099 | } | ||
| 1100 | |||
| 1101 | 1 | void OrbitCamera::Orbit(double deltaYaw, double deltaPitch) { | |
| 1102 | 1 | yaw += deltaYaw; | |
| 1103 | 1 | pitch = std::clamp(pitch + deltaPitch, kMinPitch, kMaxPitch); | |
| 1104 | 1 | } | |
| 1105 | |||
| 1106 | 1 | void OrbitCamera::Pan(double dx, double dy, double height) { | |
| 1107 | 1 | const glm::dvec3 right(std::cos(yaw), std::sin(yaw), 0.0); | |
| 1108 | 1 | const glm::dvec3 forward(-std::sin(yaw), std::cos(yaw), 0.0); | |
| 1109 | 1 | target += (-dx * right + dy * forward) * MetresPerPixel(*this, height); | |
| 1110 | 1 | } | |
| 1111 | |||
| 1112 | 1 | void OrbitCamera::PanVertical(double dx, double dy, double height) { | |
| 1113 | 1 | const glm::dvec3 right(std::cos(yaw), std::sin(yaw), 0.0); | |
| 1114 | 1 | target += (-dx * right + dy * glm::dvec3(0.0, 0.0, 1.0)) * MetresPerPixel(*this, height); | |
| 1115 | 1 | } | |
| 1116 | |||
| 1117 | 3 | void OrbitCamera::Zoom(double steps) { | |
| 1118 | 3 | distance = std::clamp(distance * std::pow(0.9, steps), kMinDistance, kMaxDistance); | |
| 1119 | 3 | } | |
| 1120 | |||
| 1121 | 5 | void OrbitCamera::Frame(const Box& box) { | |
| 1122 | 5 | if (box.Empty()) { | |
| 1123 | return; | ||
| 1124 | } | ||
| 1125 | 2 | target = box.Center(); | |
| 1126 | 2 | const double radius = std::max(0.5 * glm::length(box.max - box.min), kMinDistance); | |
| 1127 | 4 | distance = std::clamp(1.1 * radius / std::sin(0.5 * verticalFov), kMinDistance, kMaxDistance); | |
| 1128 | } | ||
| 1129 | |||
| 1130 | // Shell | ||
| 1131 | |||
| 1132 | namespace { | ||
| 1133 | |||
| 1134 | /// Hashed alone, not on the ID stack, so it is the same outside a frame. | ||
| 1135 | const ImGuiID kDockSpace = ImHashStr("ErnieDockSpace"); | ||
| 1136 | |||
| 1137 | /// Radians the camera turns per pixel dragged. | ||
| 1138 | constexpr double kOrbitRate = 0.005; | ||
| 1139 | |||
| 1140 | /// Seconds between refreshes of the heads-up display. | ||
| 1141 | constexpr double kHudRefresh = 0.25; | ||
| 1142 | |||
| 1143 | constexpr std::array kMouseButtons{ImGuiMouseButton_Left, ImGuiMouseButton_Right, | ||
| 1144 | ImGuiMouseButton_Middle}; | ||
| 1145 | |||
| 1146 | // Menu items as Omniverse Kit draws them: a left gutter holds a check on toggles in the accent | ||
| 1147 | // colour (`ImGuiCol_TextLink`), shortcuts sit muted at the right, and menus open at least 17 lines | ||
| 1148 | // wide. | ||
| 1149 | |||
| 1150 | /// Spaces before every label, where the markers go. | ||
| 1151 | constexpr const char* kGutter = " "; | ||
| 1152 | |||
| 1153 | /// @return Whether the plain item was clicked. | ||
| 1154 | ✗ | bool MenuAction(const char* label, const char* shortcut = nullptr, bool enabled = true) { | |
| 1155 | ✗ | return ImGui::MenuItem((std::string(kGutter) + label).c_str(), shortcut, false, enabled); | |
| 1156 | ✗ | } | |
| 1157 | |||
| 1158 | /// @brief A toggle, checked in the gutter when on; a click flips `on`. | ||
| 1159 | ✗ | void MenuCheck(const char* label, bool& on) { | |
| 1160 | ✗ | if (MenuAction(label)) { | |
| 1161 | ✗ | on = !on; | |
| 1162 | } | ||
| 1163 | ✗ | if (on) { | |
| 1164 | ✗ | const ImVec2 low = ImGui::GetItemRectMin(); | |
| 1165 | ✗ | const ImVec2 high = ImGui::GetItemRectMax(); | |
| 1166 | ✗ | const float size = 0.7f * ImGui::GetTextLineHeight(); | |
| 1167 | ✗ | const ImVec2 centre(low.x + 0.5f * ImGui::CalcTextSize(kGutter).x, 0.5f * (low.y + high.y)); | |
| 1168 | ✗ | ImGui::RenderCheckMark(ImGui::GetWindowDrawList(), | |
| 1169 | ✗ | ImVec2(centre.x - 0.5f * size, centre.y - 0.5f * size), | |
| 1170 | ImGui::GetColorU32(ImGuiCol_TextLink), size); | ||
| 1171 | } | ||
| 1172 | ✗ | } | |
| 1173 | |||
| 1174 | 80 | template <typename Items> void Menu(const char* name, Items&& items) { | |
| 1175 | // A menu that stays closed leaves the constraint unused. | ||
| 1176 | 80 | ImGui::SetNextWindowSizeConstraints(ImVec2(17.0f * ImGui::GetFontSize(), 0.0f), | |
| 1177 | 80 | ImVec2(FLT_MAX, FLT_MAX)); | |
| 1178 | 80 | if (ImGui::BeginMenu(name)) { | |
| 1179 | ✗ | items(); | |
| 1180 | ✗ | ImGui::EndMenu(); | |
| 1181 | } | ||
| 1182 | 80 | } | |
| 1183 | |||
| 1184 | } // namespace | ||
| 1185 | |||
| 1186 | 9 | Shell::Shell(const Scene& scene, const DeviceContext* device, const ShellOptions& options) | |
| 1187 | 18 | : scene_(scene), device_(device), theme_(options.theme), sizes_(options.docks) { | |
| 1188 | // Stage above Properties on the right; Log, Info, Devices and Profiler tabbed at the bottom. | ||
| 1189 | 18 | panels_.push_back(MakeStagePanel(scene, selection_)); | |
| 1190 | 18 | panels_.push_back(MakePropertiesPanel(scene, selection_)); | |
| 1191 | 18 | panels_.push_back(MakeLogPanel(log_, theme_)); | |
| 1192 | 18 | panels_.push_back(MakeInfoPanel(scene, device)); | |
| 1193 | 18 | panels_.push_back(MakeDevicePanel(device)); | |
| 1194 | 18 | panels_.push_back(MakeProfilerPanel(log_, options.profile)); | |
| 1195 | 9 | if (options.profile) { | |
| 1196 | ✗ | profiler::SetEnabled(true); | |
| 1197 | } | ||
| 1198 | 9 | } | |
| 1199 | |||
| 1200 | 13 | Panel* Shell::FindPanel(std::string_view title) const { | |
| 1201 | 13 | const auto found = std::ranges::find(panels_, title, &Panel::Title); | |
| 1202 | 13 | return found != panels_.end() ? found->get() : nullptr; | |
| 1203 | } | ||
| 1204 | |||
| 1205 | 51 | std::pair<ImVec2, ImVec2> Shell::Viewport() const { | |
| 1206 | 51 | if (const ImGuiDockNode* centre = ImGui::DockBuilderGetCentralNode(kDockSpace)) { | |
| 1207 | 51 | return {centre->Pos, centre->Size}; | |
| 1208 | } | ||
| 1209 | ✗ | const ImGuiViewport* main = ImGui::GetMainViewport(); | |
| 1210 | ✗ | return {main->WorkPos, main->WorkSize}; | |
| 1211 | } | ||
| 1212 | |||
| 1213 | 20 | void Shell::Draw() { | |
| 1214 | 20 | ERNIE_ZONE("Draw the UI"); | |
| 1215 | 20 | menu_.quit = menu_.resetLayout = menu_.frame = menu_.clearSelection = false; | |
| 1216 | 20 | { | |
| 1217 | 20 | ERNIE_ZONE("Draw the menus"); | |
| 1218 | 20 | DrawMenuBar(); | |
| 1219 | 20 | } | |
| 1220 | 20 | { | |
| 1221 | 20 | ERNIE_ZONE("Lay out the docks"); | |
| 1222 | 20 | DrawDockSpace(); | |
| 1223 | 20 | } | |
| 1224 | 20 | DrawPanels(); | |
| 1225 | 20 | { | |
| 1226 | 20 | ERNIE_ZONE("Draw the help windows"); | |
| 1227 | 20 | DrawControlsWindow(menu_.controlsShown); | |
| 1228 | 20 | DrawAboutWindow(menu_.aboutShown, device_ != nullptr ? device_->Info().name : ""); | |
| 1229 | 20 | if (menu_.demoShown) { | |
| 1230 | ✗ | ImGui::ShowDemoWindow(&menu_.demoShown); | |
| 1231 | } | ||
| 1232 | 20 | } | |
| 1233 | 20 | const ImGuiIO& io = ImGui::GetIO(); | |
| 1234 | 20 | if (!io.WantTextInput) { | |
| 1235 | 20 | menu_.quit |= io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Q, false); | |
| 1236 | 39 | menu_.frame |= !io.KeyCtrl && !io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_F, false); | |
| 1237 | } | ||
| 1238 | 20 | { | |
| 1239 | 20 | ERNIE_ZONE("Move the camera"); | |
| 1240 | 20 | Navigate(); | |
| 1241 | 20 | } | |
| 1242 | 20 | { | |
| 1243 | 20 | ERNIE_ZONE("Draw the HUD"); | |
| 1244 | 20 | DrawHud(); | |
| 1245 | 20 | } | |
| 1246 | 20 | Apply(); | |
| 1247 | 20 | } | |
| 1248 | |||
| 1249 | 20 | void Shell::DrawMenuBar() { | |
| 1250 | 20 | if (!ImGui::BeginMainMenuBar()) { | |
| 1251 | ✗ | return; | |
| 1252 | } | ||
| 1253 | 20 | const bool selected = !selection_.Empty(); | |
| 1254 | 20 | Menu("File", [&] { menu_.quit |= MenuAction("Quit", "Ctrl+Q"); }); | |
| 1255 | 20 | Menu("Edit", [&] { | |
| 1256 | ✗ | menu_.frame |= MenuAction(selected ? "Frame Selection" : "Frame Scene", "F"); | |
| 1257 | ✗ | menu_.clearSelection |= MenuAction("Clear Selection", nullptr, selected); | |
| 1258 | ✗ | }); | |
| 1259 | 20 | Menu("Window", [&] { | |
| 1260 | ✗ | for (const std::unique_ptr<Panel>& panel : panels_) { | |
| 1261 | ✗ | bool visible = panel->Visible(); | |
| 1262 | ✗ | MenuCheck(panel->Title().c_str(), visible); | |
| 1263 | ✗ | panel->SetVisible(visible); | |
| 1264 | } | ||
| 1265 | ✗ | ImGui::Separator(); | |
| 1266 | ✗ | menu_.resetLayout |= MenuAction("Reset Layout"); | |
| 1267 | ✗ | MenuCheck("ImGui Demo", menu_.demoShown); | |
| 1268 | ✗ | }); | |
| 1269 | 20 | Menu("Help", [&] { | |
| 1270 | ✗ | MenuCheck("Keyboard and Mouse", menu_.controlsShown); | |
| 1271 | ✗ | MenuCheck("About Ernie", menu_.aboutShown); | |
| 1272 | ✗ | }); | |
| 1273 | 20 | ImGui::EndMainMenuBar(); | |
| 1274 | } | ||
| 1275 | |||
| 1276 | 20 | void Shell::DrawDockSpace() { | |
| 1277 | 20 | if (!layoutBuilt_) { | |
| 1278 | 8 | BuildLayout(); | |
| 1279 | 8 | layoutBuilt_ = true; | |
| 1280 | } | ||
| 1281 | // The viewport shows through the central node. | ||
| 1282 | 20 | ImGui::DockSpaceOverViewport(kDockSpace, ImGui::GetMainViewport(), | |
| 1283 | ImGuiDockNodeFlags_PassthruCentralNode); | ||
| 1284 | 20 | } | |
| 1285 | |||
| 1286 | 8 | void Shell::BuildLayout() { | |
| 1287 | 8 | ImGui::DockBuilderRemoveNode(kDockSpace); | |
| 1288 | 8 | ImGui::DockBuilderAddNode(kDockSpace, ImGuiDockNodeFlags_DockSpace); | |
| 1289 | 8 | ImGui::DockBuilderSetNodeSize(kDockSpace, ImGui::GetMainViewport()->WorkSize); | |
| 1290 | 8 | docks_.clear(); | |
| 1291 | // Hidden panels take no room; they dock where they ask once shown. | ||
| 1292 | 40 | const auto wants = [&](Dock dock) { | |
| 1293 | 32 | return std::ranges::any_of(panels_, [dock](const std::unique_ptr<Panel>& panel) { | |
| 1294 | 96 | return panel->Visible() && panel->DefaultDock() == dock; | |
| 1295 | 32 | }); | |
| 1296 | 8 | }; | |
| 1297 | 32 | const auto split = [](ImGuiID& rest, ImGuiDir direction, float share) { | |
| 1298 | 48 | return ImGui::DockBuilderSplitNode(rest, direction, std::clamp(share, 0.02f, 0.98f), | |
| 1299 | 24 | nullptr, &rest); | |
| 1300 | }; | ||
| 1301 | 8 | ImGuiID centre = kDockSpace; | |
| 1302 | 8 | if (wants(Dock::Left)) { | |
| 1303 | ✗ | docks_[Dock::Left] = split(centre, ImGuiDir_Left, sizes_.left); | |
| 1304 | } | ||
| 1305 | // The right column: `Right` above `RightLower`, either taking all of it alone. | ||
| 1306 | 8 | const bool upper = wants(Dock::Right); | |
| 1307 | 8 | const bool lower = wants(Dock::RightLower); | |
| 1308 | 8 | if (upper || lower) { | |
| 1309 | 8 | ImGuiID column = split(centre, ImGuiDir_Right, sizes_.right); | |
| 1310 | 8 | if (upper && lower) { | |
| 1311 | 8 | docks_[Dock::RightLower] = split(column, ImGuiDir_Down, sizes_.rightLower); | |
| 1312 | } | ||
| 1313 | 8 | docks_[upper ? Dock::Right : Dock::RightLower] = column; | |
| 1314 | } | ||
| 1315 | 8 | if (wants(Dock::Bottom)) { | |
| 1316 | 8 | docks_[Dock::Bottom] = split(centre, ImGuiDir_Down, sizes_.bottom); | |
| 1317 | } | ||
| 1318 | 56 | for (const std::unique_ptr<Panel>& panel : panels_) { | |
| 1319 | 48 | const auto node = docks_.find(panel->DefaultDock()); | |
| 1320 | 48 | if (panel->Visible() && node != docks_.end()) { | |
| 1321 | 48 | ImGui::DockBuilderDockWindow(panel->Title().c_str(), node->second); | |
| 1322 | } | ||
| 1323 | } | ||
| 1324 | 8 | ImGui::DockBuilderFinish(kDockSpace); | |
| 1325 | // A dock opens on the last window docked into it; the bottom one opens on the Log instead. | ||
| 1326 | 8 | const Panel* log = FindPanel("Log"); | |
| 1327 | 16 | selectLog_ = log != nullptr && log->Visible() && log->DefaultDock() == Dock::Bottom && | |
| 1328 | 8 | docks_.contains(Dock::Bottom); | |
| 1329 | 8 | } | |
| 1330 | |||
| 1331 | 20 | void Shell::SelectLogTab() { | |
| 1332 | // A dock shows the focused window's tab, and ImGui focuses each window as it is made: once | ||
| 1333 | // every panel has been, the Log takes the focus. | ||
| 1334 | 20 | if (selectLog_ && ImGui::FindWindowByName("Log") != nullptr) { | |
| 1335 | 8 | ImGui::SetWindowFocus("Log"); | |
| 1336 | 8 | selectLog_ = false; | |
| 1337 | } | ||
| 1338 | 20 | } | |
| 1339 | |||
| 1340 | 20 | void Shell::DrawPanels() { | |
| 1341 | 20 | ERNIE_ZONE("Draw the panels"); | |
| 1342 | 140 | for (const std::unique_ptr<Panel>& panel : panels_) { | |
| 1343 | 120 | ERNIE_ZONE("Draw a panel"); | |
| 1344 | 120 | ERNIE_ZONE_TEXT(panel->Title()); | |
| 1345 | 120 | if (!panel->Visible()) { | |
| 1346 | 2 | panel->Undrawn(); | |
| 1347 | 2 | continue; | |
| 1348 | } | ||
| 1349 | 118 | if (const auto node = docks_.find(panel->DefaultDock()); node != docks_.end()) { | |
| 1350 | 118 | ImGui::SetNextWindowDockID(node->second, ImGuiCond_FirstUseEver); | |
| 1351 | } | ||
| 1352 | 118 | bool keep = true; | |
| 1353 | 118 | if (ImGui::Begin(panel->Title().c_str(), &keep)) { | |
| 1354 | 83 | panel->Draw(); | |
| 1355 | } else { | ||
| 1356 | 35 | panel->Undrawn(); | |
| 1357 | } | ||
| 1358 | 118 | ImGui::End(); | |
| 1359 | 118 | panel->SetVisible(keep); | |
| 1360 | 120 | } | |
| 1361 | 20 | SelectLogTab(); | |
| 1362 | 20 | } | |
| 1363 | |||
| 1364 | 20 | void Shell::Navigate() { | |
| 1365 | 20 | const ImGuiIO& io = ImGui::GetIO(); | |
| 1366 | 20 | const auto [origin, size] = Viewport(); | |
| 1367 | // Over the viewport and no window: the dock space lets the mouse through its central node. | ||
| 1368 | 20 | const bool hovered = !io.WantCaptureMouse && io.MousePos.x >= origin.x && | |
| 1369 | 22 | io.MousePos.y >= origin.y && io.MousePos.x < origin.x + size.x && | |
| 1370 | 2 | io.MousePos.y < origin.y + size.y; | |
| 1371 | 20 | const auto any = [](auto&& test) { return std::ranges::any_of(kMouseButtons, test); }; | |
| 1372 | 80 | if (!any([](ImGuiMouseButton button) { return ImGui::IsMouseDown(button); })) { | |
| 1373 | 20 | dragging_ = false; | |
| 1374 | ✗ | } else if (hovered && | |
| 1375 | ✗ | any([](ImGuiMouseButton button) { return ImGui::IsMouseClicked(button); })) { | |
| 1376 | ✗ | dragging_ = true; | |
| 1377 | } | ||
| 1378 | 20 | const double dx = io.MouseDelta.x; | |
| 1379 | 20 | const double dy = io.MouseDelta.y; | |
| 1380 | 20 | if (dragging_ && (dx != 0.0 || dy != 0.0)) { | |
| 1381 | ✗ | if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) { | |
| 1382 | ✗ | camera_.Orbit(-dx * kOrbitRate, dy * kOrbitRate); | |
| 1383 | ✗ | } else if (ImGui::IsMouseDown(ImGuiMouseButton_Right)) { | |
| 1384 | ✗ | camera_.Pan(dx, dy, size.y); | |
| 1385 | } else { | ||
| 1386 | ✗ | camera_.PanVertical(dx, dy, size.y); | |
| 1387 | } | ||
| 1388 | } | ||
| 1389 | 20 | if (hovered && io.MouseWheel != 0.0f) { | |
| 1390 | 1 | camera_.Zoom(io.MouseWheel); | |
| 1391 | } | ||
| 1392 | 20 | } | |
| 1393 | |||
| 1394 | 20 | void Shell::DrawHud() { | |
| 1395 | 20 | const auto [origin, size] = Viewport(); | |
| 1396 | 20 | const double now = ImGui::GetTime(); | |
| 1397 | 20 | if (hudRefreshed_ < 0.0 || now - hudRefreshed_ >= kHudRefresh) { | |
| 1398 | 10 | const double fps = ImGui::GetIO().Framerate; | |
| 1399 | 10 | const glm::dvec3 eye = camera_.Eye(); | |
| 1400 | 10 | const glm::dvec3& target = camera_.target; | |
| 1401 | 10 | std::array<char, 256> text{}; | |
| 1402 | 10 | std::snprintf(text.data(), text.size(), | |
| 1403 | "%.0f fps, %.2f ms\n%.0f x %.0f\nEye %.3g, %.3g, %.3g m\n" | ||
| 1404 | "Target %.3g, %.3g, %.3g m", | ||
| 1405 | 10 | fps, fps > 0.0 ? 1000.0 / fps : 0.0, static_cast<double>(size.x), | |
| 1406 | 10 | static_cast<double>(size.y), eye.x, eye.y, eye.z, target.x, target.y, | |
| 1407 | 10 | target.z); | |
| 1408 | 10 | hud_ = text.data(); | |
| 1409 | 10 | hudRefreshed_ = now; | |
| 1410 | } | ||
| 1411 | // Behind every window. | ||
| 1412 | 20 | const ImVec2 padding = ImGui::GetStyle().WindowPadding; | |
| 1413 | 20 | ImGui::GetBackgroundDrawList()->AddText(ImVec2(origin.x + padding.x, origin.y + padding.y), | |
| 1414 | 20 | ImGui::GetColorU32(ToImVec4(theme_.hud)), hud_.c_str()); | |
| 1415 | 20 | } | |
| 1416 | |||
| 1417 | 20 | void Shell::Apply() { | |
| 1418 | 20 | if (menu_.quit) { | |
| 1419 | 1 | open_ = false; | |
| 1420 | } | ||
| 1421 | 20 | if (menu_.resetLayout) { | |
| 1422 | ✗ | layoutBuilt_ = false; | |
| 1423 | } | ||
| 1424 | 20 | if (menu_.clearSelection) { | |
| 1425 | ✗ | selection_.Clear(); | |
| 1426 | } | ||
| 1427 | 20 | if (menu_.frame) { | |
| 1428 | ✗ | Frame(); | |
| 1429 | } | ||
| 1430 | 20 | } | |
| 1431 | |||
| 1432 | 4 | void Shell::Frame() { | |
| 1433 | 4 | const std::vector<Path> framed = | |
| 1434 | 7 | selection_.Empty() ? scene_.Children(Path::Root()) : selection_.Paths(); | |
| 1435 | 4 | camera_.Frame(WorldBounds(scene_, framed)); | |
| 1436 | 4 | } | |
| 1437 | |||
| 1438 | // Presenters | ||
| 1439 | |||
| 1440 | 2 | OffscreenPresenter::OffscreenPresenter(DeviceContext& context, glm::uvec2 extent) | |
| 1441 | 2 | : image_(context, | |
| 1442 | {.format = ImageFormat::Rgba8Unorm, | ||
| 1443 | .extent = {extent, 1}, | ||
| 1444 | .usage = ImageUsage::Attachment | ImageUsage::Sampled}, | ||
| 1445 | "Viewer frame"), | ||
| 1446 | 2 | drawn_(image_.Ready()) {} | |
| 1447 | |||
| 1448 | 6 | void OffscreenPresenter::BeginUiFrame(double delta) { | |
| 1449 | 6 | ImGuiIO& io = ImGui::GetIO(); | |
| 1450 | 6 | const glm::uvec3 extent = image_.Description().extent; | |
| 1451 | 6 | io.DisplaySize = {static_cast<float>(extent.x), static_cast<float>(extent.y)}; | |
| 1452 | 6 | io.DeltaTime = static_cast<float>(std::max(delta, 1e-4)); | |
| 1453 | 6 | } | |
| 1454 | |||
| 1455 | #if ERNIE_VIEWER_WINDOW | ||
| 1456 | namespace { | ||
| 1457 | |||
| 1458 | int glfwUsers = 0; | ||
| 1459 | |||
| 1460 | /// @return Whether GLFW is initialised, initialising it for one more user. | ||
| 1461 | bool AcquireGlfw() { | ||
| 1462 | if (glfwUsers == 0) { | ||
| 1463 | glfwSetErrorCallback([](int code, const char* message) { | ||
| 1464 | log::Detail("GLFW error " + std::to_string(code) + ": " + message); | ||
| 1465 | }); | ||
| 1466 | // GLFW finds Vulkan through the loader volk opened, as the device layer does. | ||
| 1467 | if (volkInitialize() != VK_SUCCESS) { | ||
| 1468 | return false; | ||
| 1469 | } | ||
| 1470 | glfwInitVulkanLoader(vkGetInstanceProcAddr); | ||
| 1471 | if (glfwInit() == GLFW_FALSE) { | ||
| 1472 | return false; | ||
| 1473 | } | ||
| 1474 | } | ||
| 1475 | ++glfwUsers; | ||
| 1476 | return true; | ||
| 1477 | } | ||
| 1478 | |||
| 1479 | void ReleaseGlfw() { | ||
| 1480 | if (--glfwUsers == 0) { | ||
| 1481 | glfwTerminate(); | ||
| 1482 | } | ||
| 1483 | } | ||
| 1484 | |||
| 1485 | template <typename T, typename Query> std::vector<T> Enumerate(Query&& query) { | ||
| 1486 | std::uint32_t count = 0; | ||
| 1487 | query(&count, nullptr); | ||
| 1488 | std::vector<T> items(count); | ||
| 1489 | query(&count, items.data()); | ||
| 1490 | return items; | ||
| 1491 | } | ||
| 1492 | |||
| 1493 | VkSurfaceKHR CreateSurface(VkInstance instance, GLFWwindow* window) { | ||
| 1494 | VkSurfaceKHR surface = VK_NULL_HANDLE; | ||
| 1495 | CheckVk(glfwCreateWindowSurface(instance, window, nullptr, &surface), | ||
| 1496 | "glfwCreateWindowSurface"); | ||
| 1497 | return surface; | ||
| 1498 | } | ||
| 1499 | |||
| 1500 | /// @return An 8-bit UNORM format: the UI's colours are sRGB-encoded already. | ||
| 1501 | VkSurfaceFormatKHR ChooseFormat(VkPhysicalDevice physical, VkSurfaceKHR surface) { | ||
| 1502 | const std::vector<VkSurfaceFormatKHR> formats = | ||
| 1503 | Enumerate<VkSurfaceFormatKHR>([&](std::uint32_t* count, VkSurfaceFormatKHR* items) { | ||
| 1504 | vkGetPhysicalDeviceSurfaceFormatsKHR(physical, surface, count, items); | ||
| 1505 | }); | ||
| 1506 | if (formats.empty()) { | ||
| 1507 | throw DeviceError(DeviceErrorKind::Unsupported, "The window's surface has no formats"); | ||
| 1508 | } | ||
| 1509 | for (const VkFormat wanted : {VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM}) { | ||
| 1510 | const auto found = std::ranges::find_if(formats, [&](const VkSurfaceFormatKHR& format) { | ||
| 1511 | return format.format == wanted && | ||
| 1512 | format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; | ||
| 1513 | }); | ||
| 1514 | if (found != formats.end()) { | ||
| 1515 | return *found; | ||
| 1516 | } | ||
| 1517 | } | ||
| 1518 | return formats.front(); | ||
| 1519 | } | ||
| 1520 | |||
| 1521 | VkPresentModeKHR ChoosePresentMode(VkPhysicalDevice physical, VkSurfaceKHR surface, bool vsync) { | ||
| 1522 | if (vsync) { | ||
| 1523 | return VK_PRESENT_MODE_FIFO_KHR; | ||
| 1524 | } | ||
| 1525 | const std::vector<VkPresentModeKHR> modes = | ||
| 1526 | Enumerate<VkPresentModeKHR>([&](std::uint32_t* count, VkPresentModeKHR* items) { | ||
| 1527 | vkGetPhysicalDeviceSurfacePresentModesKHR(physical, surface, count, items); | ||
| 1528 | }); | ||
| 1529 | for (const VkPresentModeKHR wanted : | ||
| 1530 | {VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR}) { | ||
| 1531 | if (std::ranges::find(modes, wanted) != modes.end()) { | ||
| 1532 | return wanted; | ||
| 1533 | } | ||
| 1534 | } | ||
| 1535 | // The one mode every surface supports. | ||
| 1536 | return VK_PRESENT_MODE_FIFO_KHR; | ||
| 1537 | } | ||
| 1538 | |||
| 1539 | /// @return Whether the swapchain still matches the surface after an acquire or present. | ||
| 1540 | bool Matches(VkResult result) { | ||
| 1541 | return result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR; | ||
| 1542 | } | ||
| 1543 | |||
| 1544 | } // namespace | ||
| 1545 | |||
| 1546 | bool Window::Available() { | ||
| 1547 | if (!AcquireGlfw()) { | ||
| 1548 | return false; | ||
| 1549 | } | ||
| 1550 | const bool supported = glfwVulkanSupported() == GLFW_TRUE; | ||
| 1551 | ReleaseGlfw(); | ||
| 1552 | return supported; | ||
| 1553 | } | ||
| 1554 | |||
| 1555 | std::vector<std::string> Window::InstanceExtensions() { | ||
| 1556 | if (!AcquireGlfw()) { | ||
| 1557 | return {}; | ||
| 1558 | } | ||
| 1559 | std::uint32_t count = 0; | ||
| 1560 | const char** names = glfwGetRequiredInstanceExtensions(&count); | ||
| 1561 | std::vector<std::string> extensions(names, names + (names != nullptr ? count : 0)); | ||
| 1562 | ReleaseGlfw(); | ||
| 1563 | return extensions; | ||
| 1564 | } | ||
| 1565 | |||
| 1566 | Window::Window(const std::string& title, glm::uvec2 size) { | ||
| 1567 | if (!AcquireGlfw()) { | ||
| 1568 | throw std::runtime_error("No display to open a window on; --offscreen draws without one"); | ||
| 1569 | } | ||
| 1570 | glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); | ||
| 1571 | window_ = glfwCreateWindow(static_cast<int>(size.x), static_cast<int>(size.y), title.c_str(), | ||
| 1572 | nullptr, nullptr); | ||
| 1573 | if (window_ == nullptr) { | ||
| 1574 | ReleaseGlfw(); | ||
| 1575 | throw std::runtime_error("Could not open a window"); | ||
| 1576 | } | ||
| 1577 | } | ||
| 1578 | |||
| 1579 | Window::~Window() { | ||
| 1580 | glfwDestroyWindow(window_); | ||
| 1581 | ReleaseGlfw(); | ||
| 1582 | } | ||
| 1583 | |||
| 1584 | glm::uvec2 Window::FramebufferSize() const { | ||
| 1585 | int width = 0; | ||
| 1586 | int height = 0; | ||
| 1587 | glfwGetFramebufferSize(window_, &width, &height); | ||
| 1588 | return {static_cast<std::uint32_t>(std::max(width, 0)), | ||
| 1589 | static_cast<std::uint32_t>(std::max(height, 0))}; | ||
| 1590 | } | ||
| 1591 | |||
| 1592 | float Window::ContentScale() const { | ||
| 1593 | float x = 1.0f; | ||
| 1594 | float y = 1.0f; | ||
| 1595 | glfwGetWindowContentScale(window_, &x, &y); | ||
| 1596 | return std::max(x, 1.0f); | ||
| 1597 | } | ||
| 1598 | |||
| 1599 | WindowPresenter::WindowPresenter(DeviceContext& context, std::unique_ptr<Window> window, bool vsync) | ||
| 1600 | : context_(context), window_(std::move(window)), | ||
| 1601 | surface_(CreateSurface(context.Instance(), window_->Handle())), vsync_(vsync) { | ||
| 1602 | const OnFailure destroySurface( | ||
| 1603 | [&] { vkDestroySurfaceKHR(context_.Instance(), surface_, nullptr); }); | ||
| 1604 | if (!context.Capabilities().present) { | ||
| 1605 | throw DeviceError(DeviceErrorKind::Unsupported, | ||
| 1606 | "The device cannot present to a window; --offscreen draws without one"); | ||
| 1607 | } | ||
| 1608 | VkBool32 presents = VK_FALSE; | ||
| 1609 | CheckVk(vkGetPhysicalDeviceSurfaceSupportKHR(context.Physical(), context.Main().Family(), | ||
| 1610 | surface_, &presents), | ||
| 1611 | "vkGetPhysicalDeviceSurfaceSupportKHR"); | ||
| 1612 | if (presents == VK_FALSE) { | ||
| 1613 | throw DeviceError(DeviceErrorKind::Unsupported, | ||
| 1614 | "The device's main queue cannot present to the window"); | ||
| 1615 | } | ||
| 1616 | format_ = ChooseFormat(context.Physical(), surface_); | ||
| 1617 | const OnFailure destroyHandles([&] { | ||
| 1618 | DestroyFrames(); | ||
| 1619 | vkDestroySwapchainKHR(context_.Device(), swapchain_, nullptr); | ||
| 1620 | }); | ||
| 1621 | CreateSwapchain(window_->FramebufferSize()); | ||
| 1622 | ImGui_ImplGlfw_InitForVulkan(window_->Handle(), true); | ||
| 1623 | } | ||
| 1624 | |||
| 1625 | WindowPresenter::~WindowPresenter() { | ||
| 1626 | ImGui_ImplGlfw_Shutdown(); | ||
| 1627 | DestroyFrames(); | ||
| 1628 | vkDestroySwapchainKHR(context_.Device(), swapchain_, nullptr); | ||
| 1629 | vkDestroySurfaceKHR(context_.Instance(), surface_, nullptr); | ||
| 1630 | } | ||
| 1631 | |||
| 1632 | void WindowPresenter::CreateSwapchain(glm::uvec2 extent) { | ||
| 1633 | ERNIE_ZONE("Create the swapchain"); | ||
| 1634 | VkSurfaceCapabilitiesKHR capabilities{}; | ||
| 1635 | CheckVk(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(context_.Physical(), surface_, &capabilities), | ||
| 1636 | "vkGetPhysicalDeviceSurfaceCapabilitiesKHR"); | ||
| 1637 | if (capabilities.currentExtent.width != std::numeric_limits<std::uint32_t>::max()) { | ||
| 1638 | extent_ = capabilities.currentExtent; | ||
| 1639 | } else { | ||
| 1640 | extent_ = {std::clamp(extent.x, capabilities.minImageExtent.width, | ||
| 1641 | capabilities.maxImageExtent.width), | ||
| 1642 | std::clamp(extent.y, capabilities.minImageExtent.height, | ||
| 1643 | capabilities.maxImageExtent.height)}; | ||
| 1644 | } | ||
| 1645 | std::uint32_t count = capabilities.minImageCount + 1; | ||
| 1646 | if (capabilities.maxImageCount > 0) { | ||
| 1647 | count = std::min(count, capabilities.maxImageCount); | ||
| 1648 | } | ||
| 1649 | const VkSwapchainKHR old = swapchain_; | ||
| 1650 | const VkSwapchainCreateInfoKHR info{ | ||
| 1651 | .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, | ||
| 1652 | .surface = surface_, | ||
| 1653 | .minImageCount = count, | ||
| 1654 | .imageFormat = format_.format, | ||
| 1655 | .imageColorSpace = format_.colorSpace, | ||
| 1656 | .imageExtent = extent_, | ||
| 1657 | .imageArrayLayers = 1, | ||
| 1658 | .imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, | ||
| 1659 | .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE, | ||
| 1660 | .preTransform = capabilities.currentTransform, | ||
| 1661 | .compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, | ||
| 1662 | .presentMode = ChoosePresentMode(context_.Physical(), surface_, vsync_), | ||
| 1663 | .clipped = VK_TRUE, | ||
| 1664 | .oldSwapchain = old}; | ||
| 1665 | CheckVk(vkCreateSwapchainKHR(context_.Device(), &info, nullptr, &swapchain_), | ||
| 1666 | "vkCreateSwapchainKHR"); | ||
| 1667 | if (old != VK_NULL_HANDLE) { | ||
| 1668 | vkDestroySwapchainKHR(context_.Device(), old, nullptr); | ||
| 1669 | } | ||
| 1670 | images_ = Enumerate<VkImage>([&](std::uint32_t* n, VkImage* items) { | ||
| 1671 | vkGetSwapchainImagesKHR(context_.Device(), swapchain_, n, items); | ||
| 1672 | }); | ||
| 1673 | for (const VkImage image : images_) { | ||
| 1674 | const VkImageViewCreateInfo view{ | ||
| 1675 | .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, | ||
| 1676 | .image = image, | ||
| 1677 | .viewType = VK_IMAGE_VIEW_TYPE_2D, | ||
| 1678 | .format = format_.format, | ||
| 1679 | .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}}; | ||
| 1680 | CheckVk(vkCreateImageView(context_.Device(), &view, nullptr, &views_.emplace_back()), | ||
| 1681 | "vkCreateImageView"); | ||
| 1682 | } | ||
| 1683 | const VkSemaphoreCreateInfo semaphore{.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; | ||
| 1684 | const auto create = [&](std::vector<VkSemaphore>& semaphores, std::size_t wanted) { | ||
| 1685 | while (semaphores.size() < wanted) { | ||
| 1686 | CheckVk(vkCreateSemaphore(context_.Device(), &semaphore, nullptr, | ||
| 1687 | &semaphores.emplace_back()), | ||
| 1688 | "vkCreateSemaphore"); | ||
| 1689 | } | ||
| 1690 | }; | ||
| 1691 | create(acquiredSemaphores_, images_.size() + 1); | ||
| 1692 | create(drawnSemaphores_, images_.size()); | ||
| 1693 | nextAcquired_ = 0; | ||
| 1694 | } | ||
| 1695 | |||
| 1696 | void WindowPresenter::DestroyFrames() { | ||
| 1697 | // The queue's idle once its presents have waited on their semaphores too. | ||
| 1698 | Quietly([this] { context_.Main().Borrow([](VkQueue queue) { vkQueueWaitIdle(queue); }); }); | ||
| 1699 | for (const VkImageView view : views_) { | ||
| 1700 | vkDestroyImageView(context_.Device(), view, nullptr); | ||
| 1701 | } | ||
| 1702 | for (const VkSemaphore semaphore : acquiredSemaphores_) { | ||
| 1703 | vkDestroySemaphore(context_.Device(), semaphore, nullptr); | ||
| 1704 | } | ||
| 1705 | for (const VkSemaphore semaphore : drawnSemaphores_) { | ||
| 1706 | vkDestroySemaphore(context_.Device(), semaphore, nullptr); | ||
| 1707 | } | ||
| 1708 | views_.clear(); | ||
| 1709 | acquiredSemaphores_.clear(); | ||
| 1710 | drawnSemaphores_.clear(); | ||
| 1711 | } | ||
| 1712 | |||
| 1713 | void WindowPresenter::PollEvents() { | ||
| 1714 | glfwPollEvents(); | ||
| 1715 | } | ||
| 1716 | |||
| 1717 | bool WindowPresenter::CloseRequested() const { | ||
| 1718 | return glfwWindowShouldClose(window_->Handle()) == GLFW_TRUE; | ||
| 1719 | } | ||
| 1720 | |||
| 1721 | void WindowPresenter::BeginUiFrame(double /*delta*/) { | ||
| 1722 | // The backend measures time steps and sizes itself. | ||
| 1723 | ImGui_ImplGlfw_NewFrame(); | ||
| 1724 | } | ||
| 1725 | |||
| 1726 | std::optional<FrameTarget> WindowPresenter::Acquire() { | ||
| 1727 | const glm::uvec2 size = window_->FramebufferSize(); | ||
| 1728 | if (size.x == 0 || size.y == 0) { | ||
| 1729 | return std::nullopt; | ||
| 1730 | } | ||
| 1731 | const glm::uvec2 extent(extent_.width, extent_.height); | ||
| 1732 | if (stale_ || size != extent) { | ||
| 1733 | ERNIE_ZONE("Resize the swapchain"); | ||
| 1734 | DestroyFrames(); | ||
| 1735 | CreateSwapchain(size); | ||
| 1736 | stale_ = false; | ||
| 1737 | } | ||
| 1738 | // Blocks only while every image is queued for the display, as vsync paces frames; the | ||
| 1739 | // device waits for the image itself. | ||
| 1740 | ERNIE_ZONE("Wait for a swapchain image"); | ||
| 1741 | const VkSemaphore acquired = acquiredSemaphores_[nextAcquired_]; | ||
| 1742 | std::uint32_t index = 0; | ||
| 1743 | const VkResult result = vkAcquireNextImageKHR(context_.Device(), swapchain_, | ||
| 1744 | std::numeric_limits<std::uint64_t>::max(), | ||
| 1745 | acquired, VK_NULL_HANDLE, &index); | ||
| 1746 | if (!Matches(result)) { | ||
| 1747 | if (result != VK_ERROR_OUT_OF_DATE_KHR) { | ||
| 1748 | CheckVk(result, "vkAcquireNextImageKHR"); | ||
| 1749 | } | ||
| 1750 | stale_ = true; | ||
| 1751 | return std::nullopt; | ||
| 1752 | } | ||
| 1753 | nextAcquired_ = (nextAcquired_ + 1) % acquiredSemaphores_.size(); | ||
| 1754 | acquired_ = index; | ||
| 1755 | return FrameTarget{images_[index], | ||
| 1756 | views_[index], | ||
| 1757 | glm::uvec2(extent_.width, extent_.height), | ||
| 1758 | VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, | ||
| 1759 | {acquired, drawnSemaphores_[index]}}; | ||
| 1760 | } | ||
| 1761 | |||
| 1762 | void WindowPresenter::Present(Ticket /*drawn*/) { | ||
| 1763 | if (!acquired_) { | ||
| 1764 | return; | ||
| 1765 | } | ||
| 1766 | // The device waits for the frame's drawing; the host goes on to the next. | ||
| 1767 | const VkPresentInfoKHR info{.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, | ||
| 1768 | .waitSemaphoreCount = 1, | ||
| 1769 | .pWaitSemaphores = &drawnSemaphores_[*acquired_], | ||
| 1770 | .swapchainCount = 1, | ||
| 1771 | .pSwapchains = &swapchain_, | ||
| 1772 | .pImageIndices = &*acquired_}; | ||
| 1773 | const VkResult result = context_.Main().Present(info); | ||
| 1774 | if (!Matches(result) && result != VK_ERROR_OUT_OF_DATE_KHR) { | ||
| 1775 | CheckVk(result, "vkQueuePresentKHR"); | ||
| 1776 | } | ||
| 1777 | stale_ = result != VK_SUCCESS; | ||
| 1778 | acquired_.reset(); | ||
| 1779 | } | ||
| 1780 | #endif | ||
| 1781 | |||
| 1782 | } // namespace ernie::app | ||
| 1783 |