Skip to content

Commit baa4a73

Browse files
authored
Enable async loads of Scenes (#48)
* Move DeltaTime to Glfw - Don't expose `Glfw::Window` through engine; users should not be able to access `Glfw` directly. - Return entire state in `Engine::poll()` instead of just delta time. * Add Engine::load_async Takes an optional `on_done` callback; loaded scene swapped and callback invoked on poll thread (during `poll()`). TODO: solve progress notification across the threads. * Add LoadStatus * Store only one future and progress Algorithm for deferring existing requests is buggy: get random malloc "unaligned fastbin chunk detected" crashes, attempts to root cause have been unsuccessful. Winding back to storing a single request and denying subsequent ones until the stored one is idle. * Add some comments, fixup minor stuff
1 parent a1306e4 commit baa4a73

File tree

15 files changed

+377
-100
lines changed

15 files changed

+377
-100
lines changed

lib/engine/include/facade/engine/editor/common.hpp

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,22 @@ class MainMenu : public MenuBar {
9090
///
9191
class Popup : public Openable {
9292
public:
93-
explicit Popup(char const* id, int flags = {});
93+
explicit Popup(char const* id, int flags = {}) : Popup(id, false, flags) {}
9494
~Popup();
9595

96+
static void open(char const* id);
9697
static void close_current();
98+
99+
protected:
100+
explicit Popup(char const* id, bool modal, int flags);
101+
};
102+
103+
///
104+
/// \brief RAII Dear ImGui PopupModal
105+
///
106+
class Modal : public Popup {
107+
public:
108+
explicit Modal(char const* id, int flags = {}) : Popup(id, true, flags) {}
97109
};
98110

99111
///

lib/engine/include/facade/engine/engine.hpp

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include <facade/glfw/glfw.hpp>
33
#include <facade/scene/scene.hpp>
44
#include <facade/util/time.hpp>
5+
#include <facade/util/unique_task.hpp>
56
#include <facade/vk/shader.hpp>
67

78
namespace facade {
@@ -28,6 +29,7 @@ struct EngineCreateInfo {
2829
class Engine {
2930
public:
3031
using CreateInfo = EngineCreateInfo;
32+
using State = Glfw::State;
3133

3234
Engine(Engine&&) noexcept;
3335
Engine& operator=(Engine&&) noexcept;
@@ -64,9 +66,9 @@ class Engine {
6466
///
6567
bool running() const;
6668
///
67-
/// \brief Poll events and obtain delta time
69+
/// \brief Poll events and obtain updated state
6870
///
69-
float poll();
71+
State const& poll();
7072
///
7173
/// \brief Render the scene
7274
///
@@ -77,14 +79,29 @@ class Engine {
7779
///
7880
void request_stop();
7981

82+
///
83+
/// \brief Load a GLTF scene asynchronously
84+
///
85+
/// Subsequent requests will be rejected if one is in flight
86+
///
87+
bool load_async(std::string gltf_json_path, UniqueTask<void()> on_loaded = {});
88+
///
89+
/// \brief Obtain status of in-flight async load request (if active)
90+
///
91+
LoadStatus load_status() const;
92+
93+
glm::uvec2 window_extent() const;
94+
glm::uvec2 framebuffer_extent() const;
95+
8096
Scene& scene() const;
81-
Gfx const& gfx() const;
82-
Glfw::Window const& window() const;
83-
Glfw::State const& state() const;
97+
State const& state() const;
8498
Input const& input() const;
8599
Renderer& renderer() const;
100+
GLFWwindow* window() const;
86101

87102
private:
103+
void update_load_request();
104+
88105
struct Impl;
89106
inline static Impl const* s_instance{};
90107
std::unique_ptr<Impl> m_impl{};

lib/engine/src/editor/common.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ MainMenu::~MainMenu() {
3737
if (m_open) { ImGui::EndMainMenuBar(); }
3838
}
3939

40-
Popup::Popup(char const* id, int flags) : Openable(ImGui::BeginPopup(id, flags)) {}
40+
Popup::Popup(char const* id, bool modal, int flags) : Openable(modal ? ImGui::BeginPopupModal(id, {}, flags) : ImGui::BeginPopup(id, flags)) {}
4141

4242
Popup::~Popup() {
4343
if (m_open) { ImGui::EndPopup(); }
4444
}
4545

46+
void Popup::open(char const* id) { ImGui::OpenPopup(id); }
47+
4648
void Popup::close_current() { ImGui::CloseCurrentPopup(); }
4749

4850
Menu::Menu(NotClosed<MenuBar>, char const* label, bool enabled) : Openable(ImGui::BeginMenu(label, enabled)) {}

lib/engine/src/engine.cpp

Lines changed: 114 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
#include <backends/imgui_impl_glfw.h>
22
#include <backends/imgui_impl_vulkan.h>
33
#include <imgui.h>
4+
#include <djson/json.hpp>
45
#include <facade/defines.hpp>
56
#include <facade/engine/engine.hpp>
67
#include <facade/engine/scene_renderer.hpp>
78
#include <facade/glfw/glfw_wsi.hpp>
89
#include <facade/render/renderer.hpp>
10+
#include <facade/util/data_provider.hpp>
911
#include <facade/util/error.hpp>
12+
#include <facade/util/logger.hpp>
1013
#include <facade/vk/cmd.hpp>
1114
#include <facade/vk/vk.hpp>
1215
#include <glm/gtc/color_space.hpp>
1316
#include <glm/mat4x4.hpp>
17+
#include <filesystem>
18+
#include <future>
1419

1520
namespace facade {
21+
namespace fs = std::filesystem;
22+
1623
namespace {
1724
static constexpr std::size_t command_buffers_v{1};
1825

@@ -150,6 +157,34 @@ struct RenderWindow {
150157
: window(std::move(window)), vulkan(GlfwWsi{this->window}, validation), gfx(vulkan.gfx()),
151158
renderer(gfx, this->window, gui.get(), Renderer::CreateInfo{command_buffers_v, msaa}), gui(std::move(gui)) {}
152159
};
160+
161+
bool load_gltf(Scene& out_scene, char const* path, std::atomic<LoadStatus>* out_status) {
162+
auto const provider = FileDataProvider::mount_parent_dir(path);
163+
auto json = dj::Json::from_file(path);
164+
return out_scene.load_gltf(json, provider, out_status);
165+
}
166+
167+
template <typename T>
168+
bool ready(std::future<T> const& future) {
169+
return future.valid() && future.wait_for(std::chrono::seconds{}) == std::future_status::ready;
170+
}
171+
172+
template <typename T>
173+
bool timeout(std::future<T> const& future) {
174+
return future.valid() && future.wait_for(std::chrono::seconds{}) == std::future_status::timeout;
175+
}
176+
177+
template <typename T>
178+
bool busy(std::future<T> const& future) {
179+
return future.valid() && future.wait_for(std::chrono::seconds{}) == std::future_status::deferred;
180+
}
181+
182+
struct LoadRequest {
183+
std::string path{};
184+
std::future<Scene> future{};
185+
std::atomic<LoadStatus> status{};
186+
float start_time{};
187+
};
153188
} // namespace
154189

155190
struct Engine::Impl {
@@ -158,14 +193,21 @@ struct Engine::Impl {
158193
Scene scene;
159194

160195
std::uint8_t msaa;
161-
DeltaTime dt{};
196+
197+
std::mutex mutex{};
198+
199+
struct {
200+
LoadRequest request{};
201+
UniqueTask<void()> callback{};
202+
} load{};
162203

163204
Impl(UniqueWin window, std::uint8_t msaa, bool validation)
164205
: window(std::move(window), std::make_unique<DearImGui>(), msaa, validation), renderer(this->window.gfx), scene(this->window.gfx), msaa(msaa) {
165206
s_instance = this;
166207
}
167208

168209
~Impl() {
210+
load.request.future = {};
169211
window.gfx.device.waitIdle();
170212
s_instance = {};
171213
}
@@ -189,32 +231,96 @@ void Engine::add_shader(Shader shader) { m_impl->window.renderer.add_shader(std:
189231

190232
void Engine::show(bool reset_dt) {
191233
glfwShowWindow(window());
192-
if (reset_dt) { m_impl->dt = {}; }
234+
if (reset_dt) { m_impl->window.window.get().glfw->reset_dt(); }
193235
}
194236

195237
void Engine::hide() { glfwHideWindow(window()); }
196238

197239
bool Engine::running() const { return !glfwWindowShouldClose(window()); }
198240

199-
float Engine::poll() {
200-
window().glfw->poll_events();
241+
auto Engine::poll() -> State const& {
242+
// the code in this call locks the mutex, so it's not inlined here
243+
update_load_request();
244+
// ImGui wants all widget calls within BeginFrame() / EndFrame(), so begin here
201245
m_impl->window.gui->new_frame();
202-
return m_impl->dt();
246+
m_impl->window.window.get().glfw->poll_events();
247+
return m_impl.get()->window.window.get().state();
203248
}
204249

205250
void Engine::render() {
206251
auto cb = vk::CommandBuffer{};
252+
// we skip rendering the scene if acquiring a swapchain image fails (unlikely)
207253
if (m_impl->window.renderer.next_frame({&cb, 1})) { m_impl->renderer.render(scene(), renderer(), cb); }
208254
m_impl->window.gui->end_frame();
209255
m_impl->window.renderer.render();
210256
}
211257

212258
void Engine::request_stop() { glfwSetWindowShouldClose(window(), GLFW_TRUE); }
213259

260+
glm::uvec2 Engine::window_extent() const { return m_impl->window.window.get().window_extent(); }
261+
glm::uvec2 Engine::framebuffer_extent() const { return m_impl->window.window.get().framebuffer_extent(); }
262+
263+
bool Engine::load_async(std::string gltf_json_path, UniqueTask<void()> on_loaded) {
264+
if (!fs::is_regular_file(gltf_json_path)) {
265+
// early return if file will fail to load anyway
266+
logger::error("[Engine] Invalid GLTF JSON path: [{}]", gltf_json_path);
267+
return false;
268+
}
269+
// shared state will need to be accessed, lock the mutex
270+
auto lock = std::scoped_lock{m_impl->mutex};
271+
if (m_impl->load.request.future.valid()) {
272+
// we don't support discarding in-flight requests
273+
logger::warn("[Engine] Denied attempt to load_async when a load request is already in flight");
274+
return false;
275+
}
276+
277+
// ready to start loading
278+
logger::info("[Engine] Loading GLTF [{}]...", State::to_filename(gltf_json_path));
279+
// populate load request
280+
m_impl->load.callback = std::move(on_loaded);
281+
m_impl->load.request.path = std::move(gltf_json_path);
282+
m_impl->load.request.status.store(LoadStatus::eStartingThread);
283+
m_impl->load.request.start_time = time::since_start();
284+
auto func = [path = m_impl->load.request.path, gfx = m_impl->window.gfx, status = &m_impl->load.request.status] {
285+
auto scene = Scene{gfx};
286+
if (!load_gltf(scene, path.c_str(), status)) { logger::error("[Engine] Failed to load GLTF: [{}]", path); }
287+
// return the scene even on failure, it will be empty but valid
288+
return scene;
289+
};
290+
// store future
291+
m_impl->load.request.future = std::async(std::launch::async, func);
292+
return true;
293+
}
294+
295+
LoadStatus Engine::load_status() const {
296+
auto lock = std::scoped_lock{m_impl->mutex};
297+
return m_impl->load.request.status.load();
298+
}
299+
214300
Scene& Engine::scene() const { return m_impl->scene; }
215-
Gfx const& Engine::gfx() const { return m_impl->window.gfx; }
216-
Glfw::Window const& Engine::window() const { return m_impl->window.window; }
217-
Glfw::State const& Engine::state() const { return window().state(); }
301+
GLFWwindow* Engine::window() const { return m_impl->window.window.get(); }
302+
Glfw::State const& Engine::state() const { return m_impl->window.window.get().state(); }
218303
Input const& Engine::input() const { return state().input; }
219304
Renderer& Engine::renderer() const { return m_impl->window.renderer; }
305+
306+
void Engine::update_load_request() {
307+
auto lock = std::unique_lock{m_impl->mutex};
308+
// early return if future isn't valid or is still busy
309+
if (!ready(m_impl->load.request.future)) { return; }
310+
311+
// transfer scene (under mutex lock)
312+
m_impl->scene = m_impl->load.request.future.get();
313+
// reset load status
314+
m_impl->load.request.status.store(LoadStatus::eNone);
315+
// move out the path
316+
auto path = std::move(m_impl->load.request.path);
317+
// move out the callback
318+
auto callback = std::move(m_impl->load.callback);
319+
auto const duration = time::since_start() - m_impl->load.request.start_time;
320+
// unlock mutex to prevent possible deadlock (eg callback calls load_gltf again)
321+
lock.unlock();
322+
logger::info("...GLTF [{}] loaded in [{:.2f}s]", State::to_filename(path), duration);
323+
// invoke callback
324+
if (callback) { callback(); }
325+
}
220326
} // namespace facade

lib/glfw/include/facade/glfw/glfw.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ struct Glfw {
1818

1919
std::vector<char const*> vk_extensions() const;
2020
void poll_events();
21+
void reset_dt();
2122

2223
bool operator==(Glfw const&) const = default;
2324
};
@@ -27,6 +28,9 @@ using UniqueWin = Unique<Glfw::Window, Glfw::Deleter>;
2728
struct Glfw::State {
2829
Input input{};
2930
std::vector<std::string> file_drops{};
31+
float dt{};
32+
33+
static std::string to_filename(std::string_view path);
3034
};
3135

3236
struct Glfw::Window {

lib/glfw/src/glfw.cpp

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
#include <facade/glfw/glfw.hpp>
22
#include <facade/util/error.hpp>
3+
#include <facade/util/time.hpp>
4+
#include <filesystem>
35
#include <mutex>
46
#include <unordered_map>
57

68
namespace facade {
9+
namespace fs = std::filesystem;
10+
711
namespace {
812
std::weak_ptr<Glfw> g_glfw{};
913
std::mutex g_mutex{};
1014

11-
std::unordered_map<GLFWwindow*, Glfw::State> g_states{};
15+
struct {
16+
std::unordered_map<GLFWwindow*, Glfw::State> states{};
17+
// polling / dt is shared across all windows
18+
DeltaTime dt{};
19+
} g_states{};
1220

1321
std::shared_ptr<Glfw> get_or_make_glfw() {
1422
auto lock = std::scoped_lock{g_mutex};
@@ -35,14 +43,23 @@ constexpr Action to_action(int glfw_action) {
3543
} // namespace
3644

3745
void Glfw::poll_events() {
38-
for (auto& [_, state] : g_states) {
46+
auto const dt = g_states.dt();
47+
for (auto& [_, state] : g_states.states) {
3948
state.input.keyboard.next_frame();
4049
state.input.mouse.next_frame();
4150
state.file_drops.clear();
51+
state.dt = dt;
4252
}
4353
glfwPollEvents();
4454
}
4555

56+
void Glfw::reset_dt() { g_states.dt = {}; }
57+
58+
std::string Glfw::State::to_filename(std::string_view path) {
59+
if (auto const i = path.find_last_of('/'); i != std::string_view::npos) { path = path.substr(i + 1); }
60+
return std::string{path};
61+
}
62+
4663
auto Glfw::Window::make() -> UniqueWin {
4764
auto ret = Window{};
4865
ret.glfw = get_or_make_glfw();
@@ -52,13 +69,13 @@ auto Glfw::Window::make() -> UniqueWin {
5269
ret.win = glfwCreateWindow(1, 1, "[untitled]", nullptr, nullptr);
5370
if (!ret.win) { throw InitError{"GLFW window creation failed"}; }
5471
if (glfwRawMouseMotionSupported()) { glfwSetInputMode(ret, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); }
55-
glfwSetKeyCallback(ret, [](GLFWwindow* w, int key, int, int action, int) { g_states[w].input.keyboard.on_key(key, to_action(action)); });
56-
glfwSetMouseButtonCallback(ret, [](GLFWwindow* w, int button, int action, int) { g_states[w].input.mouse.on_button(button, to_action(action)); });
57-
glfwSetCursorPosCallback(ret, [](GLFWwindow* w, double x, double y) { g_states[w].input.mouse.on_position(glm::tvec2<double>{x, y}); });
58-
glfwSetScrollCallback(ret, [](GLFWwindow* w, double x, double y) { g_states[w].input.mouse.on_scroll(glm::tvec2<double>{x, y}); });
72+
glfwSetKeyCallback(ret, [](GLFWwindow* w, int key, int, int action, int) { g_states.states[w].input.keyboard.on_key(key, to_action(action)); });
73+
glfwSetMouseButtonCallback(ret, [](GLFWwindow* w, int button, int action, int) { g_states.states[w].input.mouse.on_button(button, to_action(action)); });
74+
glfwSetCursorPosCallback(ret, [](GLFWwindow* w, double x, double y) { g_states.states[w].input.mouse.on_position(glm::tvec2<double>{x, y}); });
75+
glfwSetScrollCallback(ret, [](GLFWwindow* w, double x, double y) { g_states.states[w].input.mouse.on_scroll(glm::tvec2<double>{x, y}); });
5976
glfwSetDropCallback(ret, [](GLFWwindow* w, int count, char const** paths) {
60-
auto& file_drops = g_states[w].file_drops;
61-
for (int i = 0; i < count; ++i) { file_drops.push_back(paths[i]); }
77+
auto& file_drops = g_states.states[w].file_drops;
78+
for (int i = 0; i < count; ++i) { file_drops.push_back(fs::absolute(paths[i]).generic_string()); }
6279
});
6380
return ret;
6481
}
@@ -70,7 +87,7 @@ void Glfw::Deleter::operator()(Glfw const& glfw) const {
7087

7188
void Glfw::Deleter::operator()(Window const& window) const {
7289
glfwDestroyWindow(window.win);
73-
g_states.erase(window.win);
90+
g_states.states.erase(window.win);
7491
}
7592

7693
std::vector<char const*> Glfw::vk_extensions() const {
@@ -95,6 +112,6 @@ glm::uvec2 Glfw::Window::framebuffer_extent() const {
95112

96113
Glfw::State const& Glfw::Window::state() const {
97114
auto lock = std::scoped_lock{g_mutex};
98-
return g_states[win];
115+
return g_states.states[win];
99116
}
100117
} // namespace facade

0 commit comments

Comments
 (0)