GCC Code Coverage Report


ernie/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
687 / 989
69.5%
Branches:
301 / 843
35.7%
Functions:
78 / 109
71.6%

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
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ while (messages_.size() > limit_) {
47 ✗ messages_.pop_front();
48 }
49
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ }
50
51 10 std::unordered_set<Path> StageMatches(const Scene& scene, std::string_view filter) {
52
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 8 times.
10 std::unordered_set<Path> matches;
53
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 8 times.
10 if (filter.empty()) {
54 return matches;
55 }
56
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
2 const std::string wanted = ToLower(filter);
57
3/4
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 8 times.
✓ Branch 4 taken 2 times.
10 for (const Path& path : scene.Prims()) {
58
3/4
✓ Branch 1 taken 8 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 7 times.
✓ Branch 5 taken 1 time.
16 if (ToLower(path.String()).find(wanted) == std::string::npos) {
59 7 continue;
60 }
61
5/8
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 4 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 3 times.
✓ Branch 10 taken 1 time.
7 for (Path shown = path; !shown.IsEmpty() && !shown.IsRoot(); shown = shown.Parent()) {
62
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 3 times.
✗ Branch 4 not taken.
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
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 19 times.
19 if (ImGui::Button("Clear")) {
88 ✗ log_.Clear();
89 }
90 19 ImGui::SameLine();
91 19 const bool copy = ImGui::Button("Copy");
92
1/2
✓ Branch 1 taken 19 times.
✗ Branch 2 not taken.
19 std::string copied;
93
2/4
✓ Branch 1 taken 19 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 19 times.
✗ Branch 4 not taken.
19 if (ImGui::BeginChild("##messages", ImVec2(0.0f, 0.0f), ImGuiChildFlags_Borders)) {
94 19 const std::string_view filter(filter_.data());
95
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 19 times.
19 for (const Message& message : log_.Messages()) {
96
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (!shown_[static_cast<std::size_t>(message.severity)] ||
97
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ (!filter.empty() && message.text.find(filter) == std::string::npos)) {
98 ✗ continue;
99 }
100
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ ImGui::TextDisabled("%8.2f", message.time);
101
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ ImGui::SameLine();
102
0/4
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ ImGui::PushStyleColor(ImGuiCol_Text, Colour(message.severity));
103
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ ImGui::TextWrapped("%s", message.text.c_str());
104
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ ImGui::PopStyleColor();
105
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (copy) {
106
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ copied += message.text + "\n";
107 }
108 }
109 // Follows new messages while scrolled to the end.
110
4/6
✓ Branch 1 taken 19 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 19 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 13 times.
✓ Branch 7 taken 6 times.
19 if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 1.0f) {
111
1/2
✓ Branch 1 taken 13 times.
✗ Branch 2 not taken.
13 ImGui::SetScrollHereY(1.0f);
112 }
113 }
114
1/2
✓ Branch 1 taken 19 times.
✗ Branch 2 not taken.
19 ImGui::EndChild();
115
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 19 times.
19 if (copy) {
116
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ ImGui::SetClipboardText(copied.c_str());
117 }
118 19 }
119
120 private:
121 ✗ ImVec4 Colour(Severity severity) const {
122
0/3
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ 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
3/4
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
✓ Branch 4 taken 13 times.
20 if (ImGui::InputTextWithHint("##search", "Search", search_.data(), search_.size()) ||
147
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 13 times.
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
1/2
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
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
1/2
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
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
2/2
✓ Branch 1 taken 17 times.
✓ Branch 2 taken 20 times.
37 for (const Path& child : scene_.Children(parent)) {
173
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
17 if (!Searching() || matches_.contains(child)) {
174
1/2
✓ Branch 1 taken 17 times.
✗ Branch 2 not taken.
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
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 14 times.
17 if (leaf) {
187 3 flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
188 }
189
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
17 if (selection_.Contains(path)) {
190 ✗ flags |= ImGuiTreeNodeFlags_Selected;
191 }
192 // Searching opens the matches' ancestors.
193
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
17 if (Searching()) {
194 ✗ ImGui::SetNextItemOpen(true);
195 }
196
1/2
✓ Branch 3 taken 17 times.
✗ Branch 4 not taken.
17 const std::string name(path.Name());
197
1/2
✓ Branch 1 taken 17 times.
✗ Branch 2 not taken.
17 const bool open = ImGui::TreeNodeEx(path.String().c_str(), flags, "%s", name.c_str());
198
2/8
✓ Branch 1 taken 17 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
17 if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
199
0/4
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ if (ImGui::GetIO().KeyCtrl) {
200
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ selection_.Toggle(path);
201 } else {
202
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ selection_.Set(path);
203 }
204 }
205
3/6
✓ Branch 1 taken 17 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 17 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 17 times.
✗ Branch 8 not taken.
17 TableTexts({scene_.TypeName(scene_.Require(path))});
206
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
17 if (open && !leaf) {
207
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ DrawChildren(path);
208
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ 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
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 12 times.
22 for (const Path& path : paths) {
223
1/2
✓ Branch 1 taken 10 times.
✗ Branch 2 not taken.
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
16/30
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1 time.
✓ Branch 3 taken 8 times.
✗ Branch 4 not taken.
✓ Branch 6 taken 1 time.
✗ Branch 7 not taken.
✓ Branch 9 taken 8 times.
✗ Branch 10 not taken.
✓ Branch 12 taken 8 times.
✗ Branch 13 not taken.
✓ Branch 15 taken 8 times.
✗ Branch 16 not taken.
✓ Branch 18 taken 8 times.
✗ Branch 19 not taken.
✓ Branch 21 taken 8 times.
✓ Branch 22 taken 1 time.
✓ Branch 23 taken 8 times.
✓ Branch 24 taken 1 time.
✓ Branch 25 taken 8 times.
✓ Branch 26 taken 1 time.
✓ Branch 27 taken 8 times.
✓ Branch 28 taken 1 time.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
✗ Branch 31 not taken.
✗ Branch 32 not taken.
✗ Branch 33 not taken.
✗ Branch 34 not taken.
✗ Branch 35 not taken.
✗ Branch 36 not taken.
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
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 14 times.
20 return type.empty() ? "untyped" : type;
242 }
243
244 ✗ std::string ValueText(const Value& value) {
245 ✗ const auto numbers = [](const auto& components) {
246
0/8
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ 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
0/14
auto ernie::app::(anonymous namespace)::ValueText(ernie::Value const&)::{lambda<int $N0, typename $T1>(glm::vec<$N0, $T1, (glm::qualifier)0> const&)#1}::operator()<2, double>(glm::vec<2, double, (glm::qualifier)0> const&) const:
✗ Branch 2 not taken.
✗ Branch 3 not taken.
auto ernie::app::(anonymous namespace)::ValueText(ernie::Value const&)::{lambda<int $N0, typename $T1>(glm::vec<$N0, $T1, (glm::qualifier)0> const&)#1}::operator()<2, float>(glm::vec<2, float, (glm::qualifier)0> const&) const:
✗ Branch 2 not taken.
✗ Branch 3 not taken.
auto ernie::app::(anonymous namespace)::ValueText(ernie::Value const&)::{lambda<int $N0, typename $T1>(glm::vec<$N0, $T1, (glm::qualifier)0> const&)#1}::operator()<2, int>(glm::vec<2, int, (glm::qualifier)0> const&) const:
✗ Branch 2 not taken.
✗ Branch 3 not taken.
auto ernie::app::(anonymous namespace)::ValueText(ernie::Value const&)::{lambda<int $N0, typename $T1>(glm::vec<$N0, $T1, (glm::qualifier)0> const&)#1}::operator()<3, double>(glm::vec<3, double, (glm::qualifier)0> const&) const:
✗ Branch 2 not taken.
✗ Branch 3 not taken.
auto ernie::app::(anonymous namespace)::ValueText(ernie::Value const&)::{lambda<int $N0, typename $T1>(glm::vec<$N0, $T1, (glm::qualifier)0> const&)#1}::operator()<3, float>(glm::vec<3, float, (glm::qualifier)0> const&) const:
✗ Branch 2 not taken.
✗ Branch 3 not taken.
auto ernie::app::(anonymous namespace)::ValueText(ernie::Value const&)::{lambda<int $N0, typename $T1>(glm::vec<$N0, $T1, (glm::qualifier)0> const&)#1}::operator()<4, double>(glm::vec<4, double, (glm::qualifier)0> const&) const:
✗ Branch 2 not taken.
✗ Branch 3 not taken.
auto ernie::app::(anonymous namespace)::ValueText(ernie::Value const&)::{lambda<int $N0, typename $T1>(glm::vec<$N0, $T1, (glm::qualifier)0> const&)#1}::operator()<4, float>(glm::vec<4, float, (glm::qualifier)0> const&) const:
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ 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
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ return "[" + numbers(data) + "]";
269 } else if constexpr (std::is_same_v<T, std::vector<Path>>) {
270
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ return "[" + Join(data, ", ", &Path::String) + "]";
271 } else if constexpr (std::is_same_v<T, std::vector<std::string>>) {
272
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ 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
0/18
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ const std::string sampled = samples > 0 ? ", " + std::to_string(samples) + " samples" : "";
283
0/4
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ if (const Tensor* array = scene.Read(prim, attribute)) {
284
0/8
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ return "array of " + Join(array->Shape(), " x ", [](auto n) { return std::to_string(n); }) +
285
0/8
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ ", " + FormatBytes(array->Bytes()) + (array->OnHost() ? "" : " on a device") +
286
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ sampled;
287 }
288
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ const std::optional<Value> value = scene.Get(prim, attribute);
289
0/8
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ 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
4/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 18 times.
✓ Branch 3 taken 1 time.
✓ Branch 4 taken 1 time.
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/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 time.
1 if (const std::vector<std::string> apis = scene_.AppliedApis(prim); !apis.empty()) {
311
0/4
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ LabelValue("API schemas", Join(apis, ", "));
312 1 }
313
1/2
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
1 LabelValue("World bounds", BoxText(scene_.WorldBounds(path)));
314
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
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/2
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
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
2/4
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 time.
1 for (const std::string& attribute : scene_.Attributes(path)) {
329
0/4
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ if (ToLower(attribute).find(wanted) == std::string::npos) {
330 ✗ continue;
331 }
332
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ ImGui::TableNextRow();
333
0/4
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ TableTexts({attribute, AttributeText(scene_, prim, attribute)});
334 1 }
335
1/2
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (counted_ != scene_.Version()) {
353 8 Count();
354 }
355
1/2
✓ Branch 1 taken 8 times.
✗ Branch 2 not taken.
8 if (!ImGui::BeginTable("##info", 3, ImGuiTableFlags_SizingStretchSame)) {
356 return;
357 }
358 8 ImGui::TableNextColumn();
359 8 ImGui::SeparatorText("Scene");
360
1/2
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 LabelValue("Prims", std::to_string(prims_));
361
1/2
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 LabelValue("Bounds", BoxText(bounds_));
362
6/10
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 1 time.
✓ Branch 5 taken 7 times.
✗ Branch 6 not taken.
✓ Branch 10 taken 8 times.
✗ Branch 11 not taken.
✓ Branch 13 taken 7 times.
✓ Branch 14 taken 1 time.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
22 LabelValue("Size", bounds_.Empty() ? "none" : VectorText(bounds_.max - bounds_.min) + " m");
363
1/2
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 LabelValue("Time code", ToText(scene_.Time()));
364 8 ImGui::TableNextColumn();
365 8 ImGui::SeparatorText("Prims by type");
366
2/2
✓ Branch 1 taken 19 times.
✓ Branch 2 taken 8 times.
27 for (const auto& [type, count] : types_) {
367
1/2
✓ Branch 2 taken 19 times.
✗ Branch 3 not taken.
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
2/2
✓ Branch 1 taken 25 times.
✓ Branch 2 taken 8 times.
33 for (const Path& path : scene_.Prims()) {
380
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 if (!path.IsRoot()) {
381 25 ++prims_;
382
4/8
✓ Branch 1 taken 25 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 25 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 25 times.
✗ Branch 8 not taken.
✓ Branch 10 taken 25 times.
✗ Branch 11 not taken.
25 ++types_[std::string(scene_.TypeName(scene_.Require(path)))];
383 }
384 8 }
385
2/4
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 8 times.
✗ Branch 6 not taken.
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
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
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
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 2 times.
8 if (device_ == nullptr) {
396 6 ImGui::TextDisabled("No device in use.");
397 6 return;
398 }
399 2 const Transfers moved = device_->Transfers();
400
1/2
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 LabelValue("Uploaded", FormatBytes(moved.uploaded));
401
1/2
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 LabelValue("Read back", FormatBytes(moved.readBack));
402
1/2
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
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
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
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
3/6
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 7 taken 2 times.
✗ Branch 8 not taken.
6 LabelValue("Heap " + std::to_string(heap) + (local ? ", device" : ", host"),
411
2/4
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
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
2/4
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
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
4/4
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 20 times.
✓ Branch 3 taken 2 times.
22 for (const auto& [has, name] : all) {
444
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 6 times.
20 if (has) {
445
1/2
✓ Branch 1 taken 14 times.
✗ Branch 2 not taken.
14 names.emplace_back(name);
446 }
447 }
448
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
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
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
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
1/2
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 LabelValue("Vulkan", ApiVersion(info));
463 2 LabelValue("UUID", info.uuid);
464
1/2
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
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
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
8 if (!ImGui::CollapsingHeader("Installed")) {
470 return;
471 }
472
0/4
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ if (ImGui::Button("Refresh") || !installed_) {
473 ✗ List();
474 }
475
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (!error_.empty()) {
476 ✗ ImGui::TextWrapped("%s", error_.c_str());
477 }
478 ✗ DrawInstalled();
479 }
480
481 private:
482 ✗ void List() {
483 ✗ try {
484
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ installed_ = ListVulkanDevices();
485 ✗ error_.clear();
486
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ } catch (const DeviceError& error) {
487 ✗ installed_ = std::vector<VulkanDeviceInfo>();
488
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ error_ = error.what();
489 ✗ }
490 ✗ }
491
492 ✗ void DrawInstalled() const {
493 ✗ constexpr ImGuiTableFlags kFlags =
494 ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_Resizable;
495
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ 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
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ for (const VulkanDeviceInfo& device : *installed_) {
504 ✗ ImGui::TableNextRow();
505 ✗ TableTexts({device.name});
506 ✗ ImGui::SetItemTooltip("%s", device.uuid.c_str());
507
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ TableTexts(
508
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ {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
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
8 profiler::SetCpuEnabled(cpu_ || alwaysRecord_);
536
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
8 profiler::SetGpuEnabled(gpu_ || alwaysRecord_);
537
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 if (!paused_) {
538 // Not the newest, whose GPU zones may still be arriving.
539 8 frames_ = profiler::Frames();
540
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 frames_.resize(frames_.size() - std::min(frames_.size(), profiler::kFramesInFlight));
541 }
542 8 DrawControls();
543
1/2
✓ Branch 1 taken 8 times.
✗ Branch 2 not taken.
8 if (ImGui::BeginTabBar("##profiler")) {
544
1/2
✓ Branch 1 taken 8 times.
✗ Branch 2 not taken.
8 if (ImGui::BeginTabItem("Zones")) {
545 8 DrawZones();
546 8 ImGui::EndTabItem();
547 }
548
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
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
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
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
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
8 if (ImGui::Button("Clear")) {
575 ✗ profiler::Clear();
576 ✗ frames_.clear();
577
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ selected_.reset();
578 }
579
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (!frames_.empty()) {
580 ✗ ImGui::SameLine();
581 ✗ ImGui::TextDisabled("%zu frames, %.1f fps", frames_.size(),
582
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ 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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 averageSeconds_ = std::clamp(averageSeconds_, 0.05, 10.0);
603 8 ImGui::EndDisabled();
604
605
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
8 if (ImGui::Button("Copy Text")) {
606
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ ImGui::SetClipboardText(profiler::ReportText(report_, Filter()).c_str());
607 }
608 8 ImGui::SameLine();
609
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
8 if (ImGui::Button("Save JSON")) {
610
0/4
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Save("ernie_profile.json", profiler::ReportJson(report_));
611 }
612 8 ImGui::SameLine();
613
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
8 if (ImGui::Button("Save Trace")) {
614
0/6
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ 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
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ WriteTextFile(path, text);
624
0/6
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ log_.Info("Saved " + path.string());
625
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ } catch (const std::exception& error) {
626
0/4
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ 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
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ for (const Frame& frame : frames_) {
637 ✗ total += FrameMs(frame);
638 }
639
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ 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
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (selected_) {
646 ✗ const auto found = std::ranges::find(frames_, *selected_, &Frame::index);
647
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ 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
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ [](const Frame& f) { return !f.gpu.empty(); });
654
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (withGpu != frames_.rend()) {
655 ✗ return &*withGpu;
656 }
657
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ 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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (!average_) {
664 ✗ const Frame* frame = Inspected();
665
0/10
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ return frame != nullptr ? std::vector<Frame>{*frame} : std::vector<Frame>{};
666 }
667 8 std::vector<Frame> frames;
668
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 if (frames_.empty()) {
669 8 return frames;
670 }
671 ✗ const auto window = static_cast<std::uint64_t>(averageSeconds_ * 1e9);
672
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ for (const Frame& frame : frames_) {
673
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (frame.end + window >= frames_.back().end) {
674
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ frames.push_back(frame);
675 }
676 }
677 ✗ return frames;
678
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
8 }
679
680 8 void DrawZones() {
681
2/4
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 8 times.
✗ Branch 6 not taken.
16 report_ = profiler::Summarize(Averaged(), profiler::ThreadNames());
682 8 constexpr ImGuiTableFlags kFlags = ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
683 ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable;
684
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 6 times.
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
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 if (frames == 0) {
715 12 ImGui::TextUnformatted("(no frames)");
716 } else {
717
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ ImGui::Text("%zu frame%s", frames, frames == 1 ? "" : "s");
718 }
719 12 ImGui::PopStyleColor();
720
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 if (!shown) {
721 return;
722 }
723
1/2
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
12 for (const ReportRow& row : rows) {
732
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (row.depth > closed) {
733 ✗ continue;
734 }
735 ✗ closed = kNone;
736
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ for (; open > row.depth; --open) {
737 ✗ ImGui::TreePop();
738 }
739 ✗ ImGui::TableNextRow();
740 ✗ ImGui::TableNextColumn();
741 ✗ ImGuiTreeNodeFlags flags =
742 ImGuiTreeNodeFlags_SpanAllColumns | ImGuiTreeNodeFlags_DefaultOpen;
743
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (!row.parent) {
744 ✗ flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
745 }
746 ✗ const bool expanded = ImGui::TreeNodeEx(row.node->name.c_str(), flags);
747
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (row.parent) {
748
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ 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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 12 times.
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
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (frames_.empty()) {
781 ✗ ImGui::TextDisabled("No frames yet.");
782 ✗ return;
783 }
784 ✗ std::vector<double> times;
785
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ for (const Frame& frame : frames_) {
786
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ times.push_back(FrameMs(frame));
787 }
788 ✗ std::ranges::sort(times);
789
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ 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
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ for (const double guide : {1000.0 / 60.0, 1000.0 / 30.0}) {
795
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ const float y = origin.y + height - barHeight(guide);
796
0/4
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ draw->AddLine(ImVec2(origin.x, y), ImVec2(origin.x + width, y),
797 ImGui::GetColorU32(ImGuiCol_TextDisabled, 0.5f));
798 }
799
0/4
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ 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
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ const ImVec2 mouse = ImGui::GetIO().MousePos;
804
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ for (std::size_t i = 0; i < frames_.size(); ++i) {
805
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ const Frame& frame = frames_[i];
806 ✗ const double ms = FrameMs(frame);
807 ✗ const float x = first + bar * static_cast<float>(i);
808
0/6
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ const bool under = hovered && mouse.x >= x && mouse.x < x + bar;
809
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ const ImGuiCol colour = inspected != nullptr && frame.index == inspected->index
810
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ ? ImGuiCol_PlotHistogramHovered
811 : (under ? ImGuiCol_ButtonHovered : ImGuiCol_PlotHistogram);
812
0/6
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ draw->AddRectFilled(ImVec2(x, origin.y + height - barHeight(ms)),
813
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ ImVec2(x + std::max(bar - 1.0f, 1.0f), origin.y + height),
814 ImGui::GetColorU32(colour));
815
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (under) {
816 ✗ ImGui::SetTooltip("Frame %llu: %.2f ms",
817
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ static_cast<unsigned long long>(frame.index), ms);
818
0/4
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
819 ✗ selected_ = frame.index;
820 ✗ paused_ = true;
821 }
822 }
823 }
824
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ if (inspected != nullptr) {
825
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ ImGui::Text("Frame %llu: %.2f ms, %zu CPU zones, %zu GPU zones",
826
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ 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
2/4
void ernie::app::(anonymous namespace)::HelpWindow<ernie::app::DrawAboutWindow(bool&, std::basic_string_view<char, std::char_traits<char> >)::{lambda()#1}>(char const*, bool&, ernie::app::DrawAboutWindow(bool&, std::basic_string_view<char, std::char_traits<char> >)::{lambda()#1}&&):
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
void ernie::app::(anonymous namespace)::HelpWindow<ernie::app::DrawControlsWindow(bool&)::{lambda()#1}>(char const*, bool&, ernie::app::DrawControlsWindow(bool&)::{lambda()#1}&&):
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
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
0/4
void ernie::app::(anonymous namespace)::HelpWindow<ernie::app::DrawAboutWindow(bool&, std::basic_string_view<char, std::char_traits<char> >)::{lambda()#1}>(char const*, bool&, ernie::app::DrawAboutWindow(bool&, std::basic_string_view<char, std::char_traits<char> >)::{lambda()#1}&&):
✗ Branch 1 not taken.
✗ Branch 2 not taken.
void ernie::app::(anonymous namespace)::HelpWindow<ernie::app::DrawControlsWindow(bool&)::{lambda()#1}>(char const*, bool&, ernie::app::DrawControlsWindow(bool&)::{lambda()#1}&&):
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ 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
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
9 return std::make_unique<LogPanel>(log, theme);
866 }
867
868 9 std::unique_ptr<Panel> MakeStagePanel(const Scene& scene, Selection& selection) {
869
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
9 return std::make_unique<StagePanel>(scene, selection);
870 }
871
872 9 std::unique_ptr<Panel> MakePropertiesPanel(const Scene& scene, const Selection& selection) {
873
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
9 return std::make_unique<PropertiesPanel>(scene, selection);
874 }
875
876 9 std::unique_ptr<Panel> MakeInfoPanel(const Scene& scene, const DeviceContext* device) {
877
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
9 return std::make_unique<InfoPanel>(scene, device);
878 }
879
880 9 std::unique_ptr<Panel> MakeDevicePanel(const DeviceContext* device) {
881
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
9 return std::make_unique<DevicePanel>(device);
882 }
883
884 9 std::unique_ptr<Panel> MakeProfilerPanel(MessageLog& log, bool alwaysRecord) {
885
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
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
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ if (ImGui::BeginTable("##controls", 2,
900 ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV)) {
901
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ 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
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ 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
2/2
✓ Branch 0 taken 32 times.
✓ Branch 1 taken 8 times.
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
2/2
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 17 times.
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
3/4
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 10 times.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
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
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 6 times.
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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
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/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
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
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
3 distance = std::clamp(distance * std::pow(0.9, steps), kMinDistance, kMaxDistance);
1119 3 }
1120
1121 5 void OrbitCamera::Frame(const Box& box) {
1122
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3 times.
5 if (box.Empty()) {
1123 return;
1124 }
1125 2 target = box.Center();
1126
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 const double radius = std::max(0.5 * glm::length(box.max - box.min), kMinDistance);
1127
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
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
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ 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
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ if (MenuAction(label)) {
1161 ✗ on = !on;
1162 }
1163
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ 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
4/8
void ernie::app::(anonymous namespace)::Menu<ernie::app::Shell::DrawMenuBar()::{lambda()#1}>(char const*, ernie::app::Shell::DrawMenuBar()::{lambda()#1}&&):
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
void ernie::app::(anonymous namespace)::Menu<ernie::app::Shell::DrawMenuBar()::{lambda()#2}>(char const*, ernie::app::Shell::DrawMenuBar()::{lambda()#2}&&):
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
void ernie::app::(anonymous namespace)::Menu<ernie::app::Shell::DrawMenuBar()::{lambda()#3}>(char const*, ernie::app::Shell::DrawMenuBar()::{lambda()#3}&&):
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
void ernie::app::(anonymous namespace)::Menu<ernie::app::Shell::DrawMenuBar()::{lambda()#4}>(char const*, ernie::app::Shell::DrawMenuBar()::{lambda()#4}&&):
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
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
1/2
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
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
1/2
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
18 panels_.push_back(MakeStagePanel(scene, selection_));
1190
1/2
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
18 panels_.push_back(MakePropertiesPanel(scene, selection_));
1191
1/2
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
18 panels_.push_back(MakeLogPanel(log_, theme_));
1192
1/2
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
18 panels_.push_back(MakeInfoPanel(scene, device));
1193
1/2
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
18 panels_.push_back(MakeDevicePanel(device));
1194
1/2
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
18 panels_.push_back(MakeProfilerPanel(log_, options.profile));
1195
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (options.profile) {
1196
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ 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
2/2
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 1 time.
13 return found != panels_.end() ? found->get() : nullptr;
1203 }
1204
1205 51 std::pair<ImVec2, ImVec2> Shell::Viewport() const {
1206
1/2
✓ Branch 1 taken 51 times.
✗ Branch 2 not taken.
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
1/2
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
20 ERNIE_ZONE("Draw the menus");
1218
1/2
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
20 DrawMenuBar();
1219 20 }
1220 20 {
1221
1/2
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
20 ERNIE_ZONE("Lay out the docks");
1222
1/2
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
20 DrawDockSpace();
1223 20 }
1224
1/2
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
20 DrawPanels();
1225 20 {
1226
1/2
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
20 ERNIE_ZONE("Draw the help windows");
1227
1/2
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
20 DrawControlsWindow(menu_.controlsShown);
1228
5/8
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 14 times.
✓ Branch 3 taken 6 times.
✗ Branch 4 not taken.
✓ Branch 6 taken 14 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 20 times.
✗ Branch 10 not taken.
20 DrawAboutWindow(menu_.aboutShown, device_ != nullptr ? device_->Info().name : "");
1229
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
20 if (menu_.demoShown) {
1230
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ ImGui::ShowDemoWindow(&menu_.demoShown);
1231 }
1232 20 }
1233
1/2
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
20 const ImGuiIO& io = ImGui::GetIO();
1234
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 if (!io.WantTextInput) {
1235
4/6
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 19 times.
✓ Branch 3 taken 1 time.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 time.
20 menu_.quit |= io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_Q, false);
1236
5/8
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 1 time.
✓ Branch 2 taken 19 times.
✗ Branch 3 not taken.
✓ Branch 5 taken 19 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 19 times.
✗ Branch 8 not taken.
39 menu_.frame |= !io.KeyCtrl && !io.KeyAlt && ImGui::IsKeyPressed(ImGuiKey_F, false);
1237 }
1238 20 {
1239
1/2
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
20 ERNIE_ZONE("Move the camera");
1240
1/2
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
20 Navigate();
1241 20 }
1242 20 {
1243
1/2
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
20 ERNIE_ZONE("Draw the HUD");
1244
1/2
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
20 DrawHud();
1245 20 }
1246
1/2
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
20 Apply();
1247 20 }
1248
1249 20 void Shell::DrawMenuBar() {
1250
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
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
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ menu_.frame |= MenuAction(selected ? "Frame Selection" : "Frame Scene", "F");
1257 ✗ menu_.clearSelection |= MenuAction("Clear Selection", nullptr, selected);
1258 ✗ });
1259 20 Menu("Window", [&] {
1260
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ 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
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 12 times.
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
3/4
✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 72 times.
✓ Branch 3 taken 24 times.
96 return panel->Visible() && panel->DefaultDock() == dock;
1295 32 });
1296 8 };
1297 32 const auto split = [](ImGuiID& rest, ImGuiDir direction, float share) {
1298
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
48 return ImGui::DockBuilderSplitNode(rest, direction, std::clamp(share, 0.02f, 0.98f),
1299 24 nullptr, &rest);
1300 };
1301 8 ImGuiID centre = kDockSpace;
1302
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
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
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 if (upper || lower) {
1309 8 ImGuiID column = split(centre, ImGuiDir_Right, sizes_.right);
1310
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 if (upper && lower) {
1311 8 docks_[Dock::RightLower] = split(column, ImGuiDir_Down, sizes_.rightLower);
1312 }
1313
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 docks_[upper ? Dock::Right : Dock::RightLower] = column;
1314 }
1315
1/2
✓ Branch 1 taken 8 times.
✗ Branch 2 not taken.
8 if (wants(Dock::Bottom)) {
1316 8 docks_[Dock::Bottom] = split(centre, ImGuiDir_Down, sizes_.bottom);
1317 }
1318
2/2
✓ Branch 0 taken 48 times.
✓ Branch 1 taken 8 times.
56 for (const std::unique_ptr<Panel>& panel : panels_) {
1319 48 const auto node = docks_.find(panel->DefaultDock());
1320
2/4
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 48 times.
✗ Branch 3 not taken.
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
4/8
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 8 times.
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
3/4
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 12 times.
✓ Branch 3 taken 8 times.
✗ Branch 4 not taken.
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
2/2
✓ Branch 0 taken 120 times.
✓ Branch 1 taken 20 times.
140 for (const std::unique_ptr<Panel>& panel : panels_) {
1343
1/2
✓ Branch 1 taken 120 times.
✗ Branch 2 not taken.
120 ERNIE_ZONE("Draw a panel");
1344 120 ERNIE_ZONE_TEXT(panel->Title());
1345
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 118 times.
120 if (!panel->Visible()) {
1346
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
2 panel->Undrawn();
1347 2 continue;
1348 }
1349
1/2
✓ Branch 1 taken 118 times.
✗ Branch 2 not taken.
118 if (const auto node = docks_.find(panel->DefaultDock()); node != docks_.end()) {
1350
1/2
✓ Branch 1 taken 118 times.
✗ Branch 2 not taken.
118 ImGui::SetNextWindowDockID(node->second, ImGuiCond_FirstUseEver);
1351 }
1352 118 bool keep = true;
1353
3/4
✓ Branch 1 taken 118 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 83 times.
✓ Branch 4 taken 35 times.
118 if (ImGui::Begin(panel->Title().c_str(), &keep)) {
1354
1/2
✓ Branch 1 taken 83 times.
✗ Branch 2 not taken.
83 panel->Draw();
1355 } else {
1356
1/2
✓ Branch 1 taken 35 times.
✗ Branch 2 not taken.
35 panel->Undrawn();
1357 }
1358
1/2
✓ Branch 1 taken 118 times.
✗ Branch 2 not taken.
118 ImGui::End();
1359 118 panel->SetVisible(keep);
1360 120 }
1361
1/2
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
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
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 18 times.
20 const bool hovered = !io.WantCaptureMouse && io.MousePos.x >= origin.x &&
1369
3/6
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
22 io.MousePos.y >= origin.y && io.MousePos.x < origin.x + size.x &&
1370
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 io.MousePos.y < origin.y + size.y;
1371 20 const auto any = [](auto&& test) { return std::ranges::any_of(kMouseButtons, test); };
1372
1/2
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
80 if (!any([](ImGuiMouseButton button) { return ImGui::IsMouseDown(button); })) {
1373 20 dragging_ = false;
1374
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ } 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
1/6
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
20 if (dragging_ && (dx != 0.0 || dy != 0.0)) {
1381
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ if (ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
1382 ✗ camera_.Orbit(-dx * kOrbitRate, dy * kOrbitRate);
1383
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ } 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
4/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 18 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 1 time.
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
4/4
✓ Branch 0 taken 12 times.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 10 times.
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
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
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
2/2
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 19 times.
20 if (menu_.quit) {
1419 1 open_ = false;
1420 }
1421
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
20 if (menu_.resetLayout) {
1422 ✗ layoutBuilt_ = false;
1423 }
1424
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
20 if (menu_.clearSelection) {
1425 ✗ selection_.Clear();
1426 }
1427
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20 times.
20 if (menu_.frame) {
1428 ✗ Frame();
1429 }
1430 20 }
1431
1432 4 void Shell::Frame() {
1433 4 const std::vector<Path> framed =
1434
3/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
7 selection_.Empty() ? scene_.Children(Path::Root()) : selection_.Paths();
1435
1/2
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
4 camera_.Frame(WorldBounds(scene_, framed));
1436 4 }
1437
1438 // Presenters
1439
1440 2 OffscreenPresenter::OffscreenPresenter(DeviceContext& context, glm::uvec2 extent)
1441
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
2 : image_(context,
1442 {.format = ImageFormat::Rgba8Unorm,
1443 .extent = {extent, 1},
1444 .usage = ImageUsage::Attachment | ImageUsage::Sampled},
1445 "Viewer frame"),
1446
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 io.DisplaySize = {static_cast<float>(extent.x), static_cast<float>(extent.y)};
1452
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
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