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 | 166 | return title_; | |
| 73 | } | ||
| 74 | /// @return Where it docks in the default layout; a saved layout overrides it. | ||
| 75 | 270 | Dock DefaultDock() const { | |
| 76 | 270 | return dock_; | |
| 77 | } | ||
| 78 | 272 | bool Visible() const { | |
| 79 | 272 | return visible_; | |
| 80 | } | ||
| 81 | 120 | void SetVisible(bool visible) { | |
| 82 | 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 | 9 | class Selection { | |
| 97 | public: | ||
| 98 | const std::vector<Path>& Paths() const { | ||
| 99 | 4 | return paths_; | |
| 100 | } | ||
| 101 | 44 | bool Empty() const { | |
| 102 | 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 | 6 | paths_ = {path}; | |
| 110 | 6 | } | |
| 111 | /// @brief Adds `path`, or removes it when selected. | ||
| 112 | ✗ | void Toggle(const Path& path) { | |
| 113 | ✗ | 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 | 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 | ✗ | Add(Severity::Info, std::move(text)); | |
| 144 | ✗ | } | |
| 145 | ✗ | void Error(std::string text) { | |
| 146 | ✗ | 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 | 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 | 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 | ✗ | return log_; | |
| 353 | } | ||
| 354 | 3 | Selection& Selected() { | |
| 355 | 3 | return selection_; | |
| 356 | } | ||
| 357 | 12 | OrbitCamera& Camera() { | |
| 358 | 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 | 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 | 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 | 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 |