GCC Code Coverage Report


ernie/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
53 / 77
68.8%
Branches:
32 / 64
50.0%
Functions:
10 / 13, 4 excl
76.9%

app/ui.h
Line Branch Exec Source
1 // SPDX-License-Identifier: BSD-3-Clause
2 /// @file ui.h
3 /// @brief The viewer's UI, in sections: the panels and what they share; the theme and widgets;
4 /// the orbit camera; the shell, which draws the menu bar, the docked panels around the viewport
5 /// and the heads-up display; and the presenters frames go to. The shell draws ImGui windows only,
6 /// so it runs without a device or a window.
7 #pragma once
8
9 #include "ernie/device/image.h"
10 #include "ernie/device/queue.h"
11 #include "ernie/geometry/bounds.h"
12 #include "ernie/scene/path.h"
13
14 #include <glm/mat4x4.hpp>
15 #include <glm/vec2.hpp>
16 #include <glm/vec3.hpp>
17 #include <glm/vec4.hpp>
18 #include <imgui.h>
19
20 #include <algorithm>
21 #include <chrono>
22 #include <cstddef>
23 #include <cstdint>
24 #include <deque>
25 #include <initializer_list>
26 #include <map>
27 #include <memory>
28 #include <optional>
29 #include <string>
30 #include <string_view>
31 #include <unordered_set>
32 #include <utility>
33 #include <vector>
34
35 struct GLFWwindow;
36
37 namespace ernie {
38 class DeviceContext;
39 class Scene;
40 } // namespace ernie
41
42 namespace ernie::app {
43
44 // Panels
45
46 struct Theme;
47
48 /// @brief Where a panel docks in the default layout.
49 enum class Dock : std::uint8_t {
50 Left,
51 /// The right column's upper part; all of it when nothing docks below.
52 Right,
53 /// The right column's lower part.
54 RightLower,
55 Bottom,
56 /// Not docked: a free window.
57 Floating,
58 };
59
60 /// @brief A dockable window. Panels take what they show and edit in their constructors, and draw
61 /// with Dear ImGui in `Draw`, between the window's `Begin` and `End`.
62 class Panel {
63 public:
64 /// @param title Its title, also its id in saved layouts.
65 54 Panel(std::string title, Dock dock, bool visible = true)
66 54 : title_(std::move(title)), dock_(dock), visible_(visible) {}
67 virtual ~Panel() = default;
68 Panel(const Panel&) = delete;
69 Panel& operator=(const Panel&) = delete;
70
71 45 const std::string& Title() const {
72
1/2
✓ Branch 1 taken 118 times.
✗ Branch 2 not taken.
166 return title_;
73 }
74 /// @return Where it docks in the default layout; a saved layout overrides it.
75 270 Dock DefaultDock() const {
76
3/4
✓ Branch 0 taken 72 times.
✓ Branch 1 taken 24 times.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
270 return dock_;
77 }
78 272 bool Visible() const {
79
5/8
✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 118 times.
✓ Branch 4 taken 48 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
272 return visible_;
80 }
81 120 void SetVisible(bool visible) {
82
2/4
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
119 visible_ = visible;
83 }
84
85 virtual void Draw() = 0;
86 /// @brief Called on frames it is not drawn: closed, or a tab behind another.
87 25 virtual void Undrawn() {}
88
89 private:
90 std::string title_;
91 Dock dock_;
92 bool visible_;
93 };
94
95 /// @brief Selected prims in the order they were picked.
96
1/2
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
9 class Selection {
97 public:
98 const std::vector<Path>& Paths() const {
99
1/2
✗ Branch 2 not taken.
✓ Branch 3 taken 1 time.
4 return paths_;
100 }
101 44 bool Empty() const {
102
4/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 18 times.
44 return paths_.empty();
103 }
104 17 bool Contains(const Path& path) const {
105 17 return std::ranges::find(paths_, path) != paths_.end();
106 }
107 /// @brief Selects `path` alone.
108 3 void Set(const Path& path) {
109
2/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
6 paths_ = {path};
110
1/2
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
6 }
111 /// @brief Adds `path`, or removes it when selected.
112 ✗ void Toggle(const Path& path) {
113
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ if (const auto found = std::ranges::find(paths_, path); found != paths_.end()) {
114 ✗ paths_.erase(found);
115 } else {
116 ✗ paths_.push_back(path);
117 }
118 ✗ }
119 ✗ void Clear() {
120 ✗ paths_.clear();
121 ✗ }
122
123 private:
124 std::vector<Path> paths_;
125 };
126
127 enum class Severity : std::uint8_t { Info, Warning, Error };
128
129 ✗ struct Message {
130 /// Seconds since the log was made.
131 double time = 0.0;
132 Severity severity = Severity::Info;
133 std::string text;
134 };
135
136 /// @brief The viewer's newest messages, oldest first, up to a limit, as the Log panel lists them.
137 ✗ class MessageLog {
138 public:
139
1/2
✓ Branch 3 taken 9 times.
✗ Branch 4 not taken.
9 explicit MessageLog(std::size_t limit = 10'000) : limit_(limit) {}
140
141 void Add(Severity severity, std::string text);
142 ✗ void Info(std::string text) {
143
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Add(Severity::Info, std::move(text));
144 ✗ }
145 ✗ void Error(std::string text) {
146
0/2
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Add(Severity::Error, std::move(text));
147 ✗ }
148 ✗ void Clear() {
149 ✗ messages_.clear();
150 ✗ }
151 const std::deque<Message>& Messages() const {
152 19 return messages_;
153 }
154
155 private:
156 std::size_t limit_;
157 std::chrono::steady_clock::time_point start_ = std::chrono::steady_clock::now();
158 std::deque<Message> messages_;
159 };
160
161 /// @return The Log panel over `log`, filtered by severity and text.
162 std::unique_ptr<Panel> MakeLogPanel(MessageLog& log, const Theme& theme);
163
164 /// @return The Stage panel: the scene's prims as a tree, picking the selection.
165 std::unique_ptr<Panel> MakeStagePanel(const Scene& scene, Selection& selection);
166
167 /// @return The prims whose path contains `filter`, ignoring case, and their ancestors: what the
168 /// Stage panel lists for it. Empty for an empty filter, which lists everything.
169 std::unordered_set<Path> StageMatches(const Scene& scene, std::string_view filter);
170
171 /// @return The Properties panel: the first selected prim's type, APIs, bounds and authored
172 /// attributes, read-only.
173 std::unique_ptr<Panel> MakePropertiesPanel(const Scene& scene, const Selection& selection);
174
175 /// @return The Info panel: the scene's prims by type and bounds, and the frame time, the device's
176 /// memory budget and the bytes it has moved.
177 /// @param device The device in use; null for none.
178 std::unique_ptr<Panel> MakeInfoPanel(const Scene& scene, const DeviceContext* device);
179
180 /// @return The Devices panel: the device in use, or none, and every Vulkan device installed.
181 std::unique_ptr<Panel> MakeDevicePanel(const DeviceContext* device);
182
183 /// @return The Profiler panel over Ernie's zone profiler, saving exports to the working directory
184 /// and saying so in `log`.
185 /// @param alwaysRecord Whether zones record while the panel is hidden too.
186 std::unique_ptr<Panel> MakeProfilerPanel(MessageLog& log, bool alwaysRecord);
187
188 /// @brief Draws the Keyboard and Mouse window while `open`; its close button clears it.
189 void DrawControlsWindow(bool& open);
190
191 /// @brief Draws the About Ernie window while `open`; its close button clears it.
192 /// @param device The name of the device in use; empty for none.
193 void DrawAboutWindow(bool& open, std::string_view device);
194
195 // Theme and widgets
196
197 /// @brief Colours and metrics of the viewer's UI: a classic scientific desktop look, after PANGU
198 /// v7's GUI, with grey-green windows, white sunken fields, one-pixel edges, square corners and a
199 /// navy accent. Colours are sRGB-encoded RGBA.
200 struct Theme {
201 /// Tab bar, menu bar, behind the docks.
202 glm::vec4 background{0.788f, 0.800f, 0.773f, 1.0f};
203 /// Panel bodies.
204 glm::vec4 panel{0.831f, 0.843f, 0.816f, 1.0f};
205 /// Section bars, table headers.
206 glm::vec4 header{0.776f, 0.788f, 0.761f, 1.0f};
207 /// Input fields, combos.
208 glm::vec4 field{1.0f, 1.0f, 1.0f, 1.0f};
209 glm::vec4 fieldHovered{0.957f, 0.965f, 0.945f, 1.0f};
210 glm::vec4 button{0.863f, 0.875f, 0.847f, 1.0f};
211 /// A pressed or chosen button.
212 glm::vec4 buttonLit{0.698f, 0.761f, 0.835f, 1.0f};
213 /// Lines between areas.
214 glm::vec4 separator{0.667f, 0.678f, 0.651f, 1.0f};
215 /// Frames', tabs' and popups' edges.
216 glm::vec4 border{0.553f, 0.565f, 0.537f, 1.0f};
217 glm::vec4 text{0.0f, 0.0f, 0.0f, 1.0f};
218 /// Labels, types and disabled text.
219 glm::vec4 textMuted{0.333f, 0.345f, 0.325f, 1.0f};
220 /// Selected and hovered rows.
221 glm::vec4 selection{0.627f, 0.741f, 0.863f, 1.0f};
222 /// Checked menu items, links, active tools.
223 glm::vec4 accent{0.122f, 0.290f, 0.541f, 1.0f};
224 glm::vec4 warning{0.910f, 0.698f, 0.235f, 1.0f};
225 glm::vec4 error{0.886f, 0.341f, 0.318f, 1.0f};
226 /// Menus and popups.
227 glm::vec4 popup{0.831f, 0.843f, 0.816f, 1.0f};
228 glm::vec4 checkBox{1.0f, 1.0f, 1.0f, 1.0f};
229 glm::vec4 checkMark{0.0f, 0.0f, 0.0f, 1.0f};
230 /// The viewport's colour where nothing is rendered.
231 glm::vec4 viewport{0.18f, 0.19f, 0.20f, 1.0f};
232 /// The heads-up display's text, over the viewport.
233 glm::vec4 hud{0.902f, 0.910f, 0.894f, 1.0f};
234 /// Text size, in pixels at a content scale of 1.
235 float fontSize = 14.0f;
236 /// Corner radius of frames, tabs and popups, in pixels.
237 float rounding = 0.0f;
238 /// Fields' and buttons' edge width, in pixels at a content scale of 1.
239 float frameBorder = 1.0f;
240 /// Padding inside frames, in pixels.
241 glm::vec2 framePadding{6.0f, 3.0f};
242 /// Spacing between items, in pixels.
243 glm::vec2 itemSpacing{6.0f, 4.0f};
244 };
245
246 /// @brief Resets the current ImGui style and writes a theme into it.
247 /// @param contentScale Scales sizes and fonts, for high-density displays.
248 void ApplyTheme(const Theme& theme, float contentScale = 1.0f);
249
250 524 inline ImVec4 ToImVec4(const glm::vec4& colour) {
251 20 return {colour.r, colour.g, colour.b, colour.a};
252 }
253
254 /// @brief Draws a label and a value on one line, the value muted and wrapped to the window.
255 void LabelValue(std::string_view label, std::string_view value);
256
257 /// @brief Fills the next cells of the current table row with `texts`.
258 void TableTexts(std::initializer_list<std::string_view> texts);
259
260 /// @return Bytes as a short size with a binary unit, such as "1.5 GiB".
261 std::string FormatBytes(std::uint64_t bytes);
262
263 // Orbit camera
264
265 /// @brief The viewer's camera: an eye orbiting a target point in a z-up world, in double
266 /// precision as the scene's transforms are, so it orbits as smoothly far from the origin as at it.
267 /// Yaw turns about +z (0 looks along +y, positive turns clockwise seen from above); pitch is the
268 /// eye's elevation above the target's horizontal plane.
269
1/2
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
9 struct OrbitCamera {
270 glm::dvec3 target{0.0};
271 /// Radians.
272 double yaw = 0.0;
273 /// Radians, within [kMinPitch, kMaxPitch].
274 double pitch = 0.6;
275 /// Metres from the eye to the target, within [kMinDistance, kMaxDistance].
276 double distance = 10.0;
277 /// Radians.
278 double verticalFov = 0.785398;
279
280 /// Short of straight down and up, where the view's up vector degenerates.
281 static constexpr double kMinPitch = -1.55;
282 static constexpr double kMaxPitch = 1.55;
283 /// A millimetre to about 100,000 light-years.
284 static constexpr double kMinDistance = 1e-3;
285 static constexpr double kMaxDistance = 1e21;
286
287 glm::dvec3 Eye() const;
288 /// @return The camera-to-world transform of the eye looking at the target, +z up.
289 glm::dmat4 Transform() const;
290
291 /// @brief Turns by yaw and pitch radians, clamping pitch.
292 void Orbit(double deltaYaw, double deltaPitch);
293 /// @brief Moves the target parallel to the ground by a drag of (`dx`, `dy`) pixels in a view
294 /// `height` pixels tall, so the ground at the target follows the cursor.
295 void Pan(double dx, double dy, double height);
296 /// @brief Moves the target across the view: sideways as `Pan`, and up and down along +z.
297 void PanVertical(double dx, double dy, double height);
298 /// @brief Moves closer by `steps` scroll steps; negative moves away.
299 void Zoom(double steps);
300 /// @brief Aims at the box's centre from the current direction, at a distance where its
301 /// bounding sphere fits the vertical field of view with a small margin. An empty box leaves
302 /// the camera as it is.
303 void Frame(const Box& box);
304 };
305
306 // Shell
307
308 /// @brief The default layout's dock sizes, as fractions of the space the docks placed before them
309 /// leave (left, then the right column, then the bottom).
310 struct DockSizes {
311 float left = 0.20f;
312 float right = 0.26f;
313 /// `Dock::RightLower`'s share of the right column's height.
314 float rightLower = 0.45f;
315 float bottom = 0.25f;
316 };
317
318 struct ShellOptions {
319 Theme theme;
320 DockSizes docks;
321 /// Record profiler zones every frame, not only while the Profiler panel shows.
322 bool profile = false;
323 };
324
325 /// @brief The panels and controls over a scene. Panels read the scene; its owner keeps it updated
326 /// (`Scene::Update`) between frames.
327 ///
328 /// Panels dock around the dock space's central node, which the viewport shows through. The default
329 /// layout is built on the first frame and after Window > Reset Layout; panels shown later dock
330 /// where they ask. Over the viewport, a left drag orbits the camera, a right drag pans along the
331 /// ground, a middle drag pans across the view and the wheel zooms; a drag that starts over the
332 /// viewport keeps control until its button is released.
333 class Shell {
334 public:
335 /// @param device The device in use, for the Devices panel; null for none.
336 Shell(const Scene& scene, const DeviceContext* device, const ShellOptions& options = {});
337
338 /// @brief Draws this frame's UI, between `ImGui::NewFrame` and `ImGui::Render`.
339 void Draw();
340 /// @return Whether the viewer should keep running: false once the user quits.
341 13 bool Open() const {
342
3/6
ernie::app::MessageLog::Info(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >):
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
ernie::app::Selection::Set(ernie::Path const&):
✗ Branch 0 not taken.
✓ Branch 1 taken 1 time.
13 return open_;
343 }
344 /// @brief Aims the camera at the selection, or at the whole scene when nothing is selected.
345 void Frame();
346
347 /// @return The viewport's position and size on the screen: the dock space's central node.
348 std::pair<ImVec2, ImVec2> Viewport() const;
349 /// @return The panel titled `title`, or null.
350 Panel* FindPanel(std::string_view title) const;
351 ✗ MessageLog& Log() {
352
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ return log_;
353 }
354 3 Selection& Selected() {
355
3/6
✓ Branch 1 taken 1 time.
✗ Branch 2 not taken.
✓ Branch 4 taken 1 time.
✗ Branch 5 not taken.
✓ Branch 7 taken 1 time.
✗ Branch 8 not taken.
3 return selection_;
356 }
357 12 OrbitCamera& Camera() {
358
1/2
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
12 return camera_;
359 }
360 const Theme& GetTheme() const {
361 return theme_;
362 }
363
364 private:
365 /// @brief What the menus ask for this frame, and the windows they show.
366
1/2
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
9 struct MenuChoices {
367 bool quit = false;
368 bool resetLayout = false;
369 bool frame = false;
370 bool clearSelection = false;
371 bool controlsShown = false;
372 bool aboutShown = false;
373 bool demoShown = false;
374 };
375
376 void DrawMenuBar();
377 void DrawDockSpace();
378 void BuildLayout();
379 void DrawPanels();
380 void SelectLogTab();
381 void Navigate();
382 void DrawHud();
383 void Apply();
384
385 const Scene& scene_;
386 const DeviceContext* device_;
387 Theme theme_;
388 DockSizes sizes_;
389 MessageLog log_;
390 Selection selection_;
391 OrbitCamera camera_;
392 MenuChoices menu_;
393 std::vector<std::unique_ptr<Panel>> panels_;
394 /// The default layout's nodes; empty until it is built.
395 std::map<Dock, ImGuiID> docks_;
396 bool layoutBuilt_ = false;
397 /// Whether the bottom dock still has to open on the Log, once its tab bar exists.
398 bool selectLog_ = false;
399 /// Whether a mouse drag that started over the viewport moves the camera.
400 bool dragging_ = false;
401 /// The heads-up display's lines, refreshed a few times a second so they stay readable.
402 std::string hud_;
403 double hudRefreshed_ = -1.0;
404 bool open_ = true;
405 };
406
407 // Presenters
408
409 /// @brief An image to draw one frame into, and the layout to leave it in.
410 struct FrameTarget {
411 VkImage image = VK_NULL_HANDLE;
412 VkImageView view = VK_NULL_HANDLE;
413 glm::uvec2 extent{0};
414 VkImageLayout finalLayout = VK_IMAGE_LAYOUT_GENERAL;
415 /// For the frame's submission: waiting until the image is free to draw into, and signalling
416 /// it drawn for presenting; none offscreen.
417 BinarySemaphores semaphores;
418 };
419
420 /// @brief Supplies the images frames are drawn into and the UI's platform input, then shows
421 /// them.
422
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
2 class Presenter {
423 public:
424 ✗ virtual ~Presenter() = default;
425
426 virtual VkFormat Format() const = 0;
427 /// @return How many images it cycles through, at least 2.
428 virtual std::uint32_t ImageCount() const = 0;
429 /// @brief Takes the window system's events.
430 virtual void PollEvents() = 0;
431 /// @return Whether the user asked to close it.
432 virtual bool CloseRequested() const = 0;
433 /// @brief Starts the UI frame's platform part: display size, time step and input.
434 virtual void BeginUiFrame(double delta) = 0;
435 /// @return The next image, or none when there is nothing to draw into, as while minimised.
436 virtual std::optional<FrameTarget> Acquire() = 0;
437 /// @brief Shows the image last acquired once `drawn` is done, or once its target's semaphore
438 /// is signalled.
439 virtual void Present(Ticket drawn) = 0;
440 };
441
442 /// @brief Draws every frame into one image of a fixed size, for tests and headless runs; the UI
443 /// gets no input.
444 class OffscreenPresenter final : public Presenter {
445 public:
446 OffscreenPresenter(DeviceContext& context, glm::uvec2 extent);
447
448 2 VkFormat Format() const override {
449 2 return ToVkFormat(image_.Description().format);
450 }
451 10 std::uint32_t ImageCount() const override {
452 10 return 2;
453 }
454 6 void PollEvents() override {}
455 6 bool CloseRequested() const override {
456 6 return false;
457 }
458 void BeginUiFrame(double delta) override;
459 6 std::optional<FrameTarget> Acquire() override {
460 // The previous frame's drawing is ordered before this one's on the same queue.
461 6 return FrameTarget{image_.Handle(), image_.View(), glm::uvec2(image_.Description().extent),
462 6 VK_IMAGE_LAYOUT_GENERAL};
463 }
464 6 void Present(Ticket drawn) override {
465 6 drawn_ = drawn;
466 6 }
467
468 /// @return The image frames are drawn into, in the general layout.
469 const DeviceImage& Image() const {
470 2 return image_;
471 }
472 /// @return The ticket of the last frame drawn.
473 2 Ticket Drawn() const {
474
1/2
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
2 return drawn_;
475 }
476
477 private:
478 DeviceImage image_;
479 Ticket drawn_;
480 };
481
482 #if ERNIE_VIEWER_WINDOW
483 /// @brief A resizable desktop window through GLFW, with no graphics API of its own. GLFW is
484 /// initialised by the first window and terminated with the last.
485 class Window {
486 public:
487 /// @throws std::runtime_error when there is no display or the window cannot be made.
488 Window(const std::string& title, glm::uvec2 size);
489 ~Window();
490 Window(const Window&) = delete;
491 Window& operator=(const Window&) = delete;
492
493 /// @return Whether a window could open: GLFW initialises, finding a display, and Vulkan can
494 /// present to it.
495 static bool Available();
496 /// @return The instance extensions presenting to a window needs.
497 static std::vector<std::string> InstanceExtensions();
498
499 /// @return The drawable size in pixels; zero while minimised.
500 glm::uvec2 FramebufferSize() const;
501 /// @return The display's scale over 96 dpi, at least 1.
502 float ContentScale() const;
503 GLFWwindow* Handle() const {
504 return window_;
505 }
506
507 private:
508 GLFWwindow* window_ = nullptr;
509 };
510
511 /// @brief Presents to a swapchain of the window's surface, made again whenever the window's size
512 /// changes, and feeds the window's input to ImGui, whose context must outlive it. The host waits
513 /// for each acquired image and each drawn frame, so presenting needs no semaphores beside the
514 /// main queue's timeline.
515 class WindowPresenter final : public Presenter {
516 public:
517 /// @param window Made before the device context, which needs its instance extensions.
518 /// @throws DeviceError when the device cannot present to the window.
519 WindowPresenter(DeviceContext& context, std::unique_ptr<Window> window, bool vsync);
520 ~WindowPresenter() override;
521
522 VkFormat Format() const override {
523 return format_.format;
524 }
525 std::uint32_t ImageCount() const override {
526 return static_cast<std::uint32_t>(images_.size());
527 }
528 void PollEvents() override;
529 bool CloseRequested() const override;
530 void BeginUiFrame(double delta) override;
531 std::optional<FrameTarget> Acquire() override;
532 void Present(Ticket drawn) override;
533
534 private:
535 /// @brief Makes the swapchain, again when there is one, at `extent`, with its views and
536 /// semaphores.
537 void CreateSwapchain(glm::uvec2 extent);
538 /// @brief Destroys the images' views and semaphores, once the queue has finished with them.
539 void DestroyFrames();
540
541 DeviceContext& context_;
542 std::unique_ptr<Window> window_;
543 VkSurfaceKHR surface_;
544 bool vsync_;
545 VkSurfaceFormatKHR format_{};
546 VkExtent2D extent_{};
547 VkSwapchainKHR swapchain_ = VK_NULL_HANDLE;
548 std::vector<VkImage> images_;
549 std::vector<VkImageView> views_;
550 /// Signalled by acquiring, one more than the images, used in turn: a frame reuses one only
551 /// after the frames in flight before it are done.
552 std::vector<VkSemaphore> acquiredSemaphores_;
553 std::size_t nextAcquired_ = 0;
554 /// Signalled when an image is drawn, one an image, for presenting it to wait on.
555 std::vector<VkSemaphore> drawnSemaphores_;
556 /// The image acquired and not yet presented.
557 std::optional<std::uint32_t> acquired_;
558 /// Whether the swapchain no longer matches the surface.
559 bool stale_ = false;
560 };
561 #endif
562
563 } // namespace ernie::app
564