Skip to content

Commit aebf5f1

Browse files
committed
ScopedWaiter
1 parent 4b54866 commit aebf5f1

File tree

7 files changed

+161
-1
lines changed

7 files changed

+161
-1
lines changed

guide/src/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@
1414
- [Vulkan Surface](initialization/surface.md)
1515
- [Vulkan Physical Device](initialization/gpu.md)
1616
- [Vulkan Device](initialization/device.md)
17+
- [Scoped Waiter](initialization/scoped_waiter.md)

guide/src/initialization/device.md

+35
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,38 @@ Declare a `vk::Queue` member (order doesn't matter since it's just a handle, the
5757
static constexpr std::uint32_t queue_index_v{0};
5858
m_queue = m_device->getQueue(m_gpu.queue_family, queue_index_v);
5959
```
60+
61+
## ScopedWaiter
62+
63+
A useful abstraction to have is an object that in its destructor waits/blocks until the Device is idle. Being able to do arbitary things on scope exit is useful in general too, but it requires some custom class template like `UniqueResource<Type, Deleter>`. We shall "abuse" `std::unique_ptr<Type, Deleter>` instead: it will not manage the pointer (`Type*`) at all, but instead `Deleter` will call a member function on it (if it isn't null).
64+
65+
Adding this to a new header `scoped_waiter.hpp`:
66+
67+
```cpp
68+
class ScopedWaiter {
69+
public:
70+
ScopedWaiter() = default;
71+
72+
explicit ScopedWaiter(vk::Device const* device) : m_device(device) {}
73+
74+
private:
75+
struct Deleter {
76+
void operator()(vk::Device const* device) const noexcept {
77+
if (device == nullptr) { return; }
78+
device->waitIdle();
79+
}
80+
};
81+
std::unique_ptr<vk::Device const, Deleter> m_device{};
82+
};
83+
```
84+
85+
This requires the passed `vk::Device*` to outlive itself, so to be defensive we make `App` be non-moveable and non-copiable, and create a member factory function for waiters:
86+
87+
```cpp
88+
auto operator=(App&&) = delete;
89+
// ...
90+
91+
[[nodiscard]] auto create_waiter() const -> ScopedWaiter {
92+
return ScopedWaiter{&*m_device};
93+
}
94+
```
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Scoped Waiter
2+
3+
A useful abstraction to have is an object that in its destructor waits/blocks until the Device is idle. It is incorrect usage to destroy Vulkan objects while they are in use by the GPU, such an object helps with making sure the device is idle before some dependent resource gets destroyed.
4+
5+
Being able to do arbitary things on scope exit will be useful in other spots too, so we encapsulate that in a basic class template `Scoped`. It's somewhat like a `unique_ptr<Type, Deleter>` that stores the value (`Type`) instead of a pointer (`Type*`), with some constraints:
6+
7+
1. `Type` must be default constructible
8+
1. Assumes a default constructed `Type` is equivalent to null (does not call `Deleter`)
9+
10+
```cpp
11+
template <typename Type>
12+
concept Scopeable =
13+
std::equality_comparable<Type> && std::is_default_constructible_v<Type>;
14+
15+
template <Scopeable Type, typename Deleter>
16+
class Scoped {
17+
public:
18+
Scoped(Scoped const&) = delete;
19+
auto operator=(Scoped const&) = delete;
20+
21+
Scoped() = default;
22+
23+
constexpr Scoped(Scoped&& rhs) noexcept
24+
: m_t(std::exchange(rhs.m_t, Type{})) {}
25+
26+
constexpr auto operator=(Scoped&& rhs) noexcept -> Scoped& {
27+
if (&rhs != this) { std::swap(m_t, rhs.m_t); }
28+
return *this;
29+
}
30+
31+
explicit constexpr Scoped(Type t) : m_t(std::move(t)) {}
32+
33+
constexpr ~Scoped() {
34+
if (m_t == Type{}) { return; }
35+
Deleter{}(m_t);
36+
}
37+
38+
[[nodiscard]] auto get() const -> Type const& { return m_t; }
39+
[[nodiscard]] auto get() -> Type& { return m_t; }
40+
41+
private:
42+
Type m_t{};
43+
};
44+
```
45+
46+
Don't worry if this doesn't make a lot of sense: the implementation isn't important, what it does and how to use it is what matters.
47+
48+
A `ScopedWaiter` can now be implemented quite easily:
49+
50+
```cpp
51+
struct ScopedWaiterDeleter {
52+
void operator()(vk::Device const device) const noexcept {
53+
device.waitIdle();
54+
}
55+
};
56+
57+
using ScopedWaiter = Scoped<vk::Device, ScopedWaiterDeleter>;
58+
```
59+
60+
Add a `ScopedWaiter` member to `App` _at the end_ of its member list: this must remain at the end to be the first member that gets destroyed, thus guaranteeing the device will be idle before the destruction of any other members begins. Initialize it after creating the Device:
61+
62+
```cpp
63+
m_waiter = ScopedWaiter{*m_device};
64+
```

src/app.cpp

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#include <app.hpp>
22
#include <print>
33

4+
#include <thread>
5+
46
VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE
57

68
namespace lvk {
@@ -79,12 +81,15 @@ void App::create_device() {
7981
static constexpr std::uint32_t queue_index_v{0};
8082
m_queue = m_device->getQueue(m_gpu.queue_family, queue_index_v);
8183

82-
// m_device_block.get() = *m_device;
84+
m_waiter = ScopedWaiter{*m_device};
8385
}
8486

8587
void App::main_loop() {
88+
auto count = 0;
8689
while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) {
8790
glfwPollEvents();
91+
if (++count > 500) { break; }
92+
std::this_thread::sleep_for(std::chrono::milliseconds{10});
8893
}
8994
}
9095
} // namespace lvk

src/app.hpp

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22
#include <gpu.hpp>
3+
#include <scoped_waiter.hpp>
34
#include <vulkan/vulkan.hpp>
45
#include <window.hpp>
56

@@ -23,5 +24,7 @@ class App {
2324
Gpu m_gpu{};
2425
vk::UniqueDevice m_device{};
2526
vk::Queue m_queue{};
27+
28+
ScopedWaiter m_waiter{};
2629
};
2730
} // namespace lvk

src/scoped.hpp

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#pragma once
2+
#include <concepts>
3+
#include <utility>
4+
5+
namespace lvk {
6+
template <typename Type>
7+
concept Scopeable =
8+
std::equality_comparable<Type> && std::is_default_constructible_v<Type>;
9+
10+
template <Scopeable Type, typename Deleter>
11+
class Scoped {
12+
public:
13+
Scoped(Scoped const&) = delete;
14+
auto operator=(Scoped const&) = delete;
15+
16+
Scoped() = default;
17+
18+
constexpr Scoped(Scoped&& rhs) noexcept
19+
: m_t(std::exchange(rhs.m_t, Type{})) {}
20+
21+
constexpr auto operator=(Scoped&& rhs) noexcept -> Scoped& {
22+
if (&rhs != this) { std::swap(m_t, rhs.m_t); }
23+
return *this;
24+
}
25+
26+
explicit constexpr Scoped(Type t) : m_t(std::move(t)) {}
27+
28+
constexpr ~Scoped() {
29+
if (m_t == Type{}) { return; }
30+
Deleter{}(m_t);
31+
}
32+
33+
[[nodiscard]] auto get() const -> Type const& { return m_t; }
34+
[[nodiscard]] auto get() -> Type& { return m_t; }
35+
36+
private:
37+
Type m_t{};
38+
};
39+
} // namespace lvk

src/scoped_waiter.hpp

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#pragma once
2+
#include <scoped.hpp>
3+
#include <vulkan/vulkan.hpp>
4+
5+
namespace lvk {
6+
struct ScopedWaiterDeleter {
7+
void operator()(vk::Device const device) const noexcept {
8+
device.waitIdle();
9+
}
10+
};
11+
12+
using ScopedWaiter = Scoped<vk::Device, ScopedWaiterDeleter>;
13+
} // namespace lvk

0 commit comments

Comments
 (0)