Skip to content

Commit 154c00f

Browse files
authored
Add and use ThreadPool (#63)
* Add Scene::Loader - Remove now unused headers from scene.hpp. - Move all loading logic into `Scene::Loader`. - WIP: temp code with futures commented out. * Add ThreadPool - Use for loading scenes and decompressing images. - Rename `Scene::Loader` to `Scene::GltfLoader`. * Upload Vulkan resources using ThreadPool * Add docs * Increment load status inside async task - More accurate status reporting. * Robust thread count - `ThreadPool`: Skip creating any threads if requested count or hardware concurrency is 0. - `Engine`: special-case thread count being 0 or 1. * Acquire image under mutex lock - Not seeing any issues without it, but best to assume WSI can interfere with device queues. * Expose force-threads as Cli Opt * Display load thread count in frame stats
1 parent 6f49513 commit 154c00f

File tree

19 files changed

+418
-195
lines changed

19 files changed

+418
-195
lines changed

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22
#include <facade/build_version.hpp>
33
#include <facade/glfw/glfw.hpp>
4+
#include <facade/scene/load_status.hpp>
45
#include <facade/scene/scene.hpp>
56
#include <facade/util/time.hpp>
67
#include <facade/util/unique_task.hpp>
@@ -20,6 +21,7 @@ struct EngineCreateInfo {
2021
std::uint8_t desired_msaa{2};
2122
bool auto_show{false};
2223
Validation validation{Validation::eDefault};
24+
std::optional<std::uint32_t> force_thread_count{};
2325
};
2426

2527
///
@@ -81,15 +83,19 @@ class Engine {
8183
void request_stop();
8284

8385
///
84-
/// \brief Load a GLTF scene asynchronously
86+
/// \brief Load a GLTF scene asynchronously.
8587
///
8688
/// Subsequent requests will be rejected if one is in flight
8789
///
8890
bool load_async(std::string gltf_json_path, UniqueTask<void()> on_loaded = {});
8991
///
90-
/// \brief Obtain status of in-flight async load request (if active)
92+
/// \brief Obtain status of in-flight async load request (if active).
9193
///
9294
LoadStatus load_status() const;
95+
///
96+
/// \brief Obtain the number of loading threads.
97+
///
98+
std::size_t load_thread_count() const;
9399

94100
glm::ivec2 window_position() const;
95101
glm::uvec2 window_extent() const;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22
#include <facade/render/renderer.hpp>
33
#include <facade/scene/scene.hpp>
4+
#include <facade/vk/buffer.hpp>
45

56
namespace facade {
67
class SceneRenderer {

lib/engine/src/engine.cpp

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,17 @@
77
#include <facade/engine/scene_renderer.hpp>
88
#include <facade/glfw/glfw_wsi.hpp>
99
#include <facade/render/renderer.hpp>
10+
#include <facade/scene/gltf_loader.hpp>
1011
#include <facade/util/data_provider.hpp>
1112
#include <facade/util/env.hpp>
1213
#include <facade/util/error.hpp>
1314
#include <facade/util/logger.hpp>
15+
#include <facade/util/thread_pool.hpp>
1416
#include <facade/vk/cmd.hpp>
1517
#include <facade/vk/vk.hpp>
1618
#include <glm/gtc/color_space.hpp>
1719
#include <glm/mat4x4.hpp>
1820
#include <filesystem>
19-
#include <future>
2021

2122
namespace facade {
2223
namespace fs = std::filesystem;
@@ -159,10 +160,10 @@ struct RenderWindow {
159160
renderer(gfx, this->window, gui.get(), Renderer::CreateInfo{command_buffers_v, msaa}), gui(std::move(gui)) {}
160161
};
161162

162-
bool load_gltf(Scene& out_scene, char const* path, AtomicLoadStatus* out_status) {
163+
bool load_gltf(Scene& out_scene, char const* path, AtomicLoadStatus& out_status, ThreadPool* thread_pool) {
163164
auto const provider = FileDataProvider::mount_parent_dir(path);
164165
auto json = dj::Json::from_file(path);
165-
return out_scene.load_gltf(json, provider, out_status);
166+
return Scene::GltfLoader{out_scene, out_status}(json, provider, thread_pool);
166167
}
167168

168169
template <typename T>
@@ -195,15 +196,17 @@ struct Engine::Impl {
195196

196197
std::uint8_t msaa;
197198

199+
ThreadPool thread_pool{};
198200
std::mutex mutex{};
199201

200202
struct {
201203
LoadRequest request{};
202204
UniqueTask<void()> callback{};
203205
} load{};
204206

205-
Impl(UniqueWin window, std::uint8_t msaa, bool validation)
206-
: window(std::move(window), std::make_unique<DearImGui>(), msaa, validation), renderer(this->window.gfx), scene(this->window.gfx), msaa(msaa) {
207+
Impl(UniqueWin window, std::uint8_t msaa, bool validation, std::optional<std::uint32_t> thread_count)
208+
: window(std::move(window), std::make_unique<DearImGui>(), msaa, validation), renderer(this->window.gfx), scene(this->window.gfx), msaa(msaa),
209+
thread_pool(thread_count) {
207210
s_instance = this;
208211
load.request.status.reset();
209212
}
@@ -225,7 +228,8 @@ bool Engine::is_instance_active() { return s_instance != nullptr; }
225228

226229
Engine::Engine(CreateInfo const& info) noexcept(false) {
227230
if (s_instance) { throw Error{"Engine: active instance exists and has not been destroyed"}; }
228-
m_impl = std::make_unique<Impl>(make_window(info.extent, info.title), info.desired_msaa, determine_validation(info.validation));
231+
if (info.force_thread_count) { logger::info("[Engine] Forcing load thread count: [{}]", *info.force_thread_count); }
232+
m_impl = std::make_unique<Impl>(make_window(info.extent, info.title), info.desired_msaa, determine_validation(info.validation), info.force_thread_count);
229233
if (info.auto_show) { show(true); }
230234
}
231235

@@ -269,6 +273,18 @@ bool Engine::load_async(std::string gltf_json_path, UniqueTask<void()> on_loaded
269273
logger::error("[Engine] Invalid GLTF JSON path: [{}]", gltf_json_path);
270274
return false;
271275
}
276+
277+
// ensure thread pool queue has at least one worker thread, else load on this thread
278+
if (m_impl->thread_pool.thread_count() == 0) {
279+
auto const start = time::since_start();
280+
if (!load_gltf(m_impl->scene, gltf_json_path.c_str(), m_impl->load.request.status, nullptr)) {
281+
logger::error("[Engine] Failed to load GLTF: [{}]", gltf_json_path);
282+
return false;
283+
}
284+
logger::info("...GLTF [{}] loaded in [{:.2f}s]", env::to_filename(gltf_json_path), time::since_start() - start);
285+
return true;
286+
}
287+
272288
// shared state will need to be accessed, lock the mutex
273289
auto lock = std::scoped_lock{m_impl->mutex};
274290
if (m_impl->load.request.future.valid()) {
@@ -284,14 +300,16 @@ bool Engine::load_async(std::string gltf_json_path, UniqueTask<void()> on_loaded
284300
m_impl->load.request.path = std::move(gltf_json_path);
285301
m_impl->load.request.status.reset();
286302
m_impl->load.request.start_time = time::since_start();
287-
auto func = [path = m_impl->load.request.path, gfx = m_impl->window.gfx, status = &m_impl->load.request.status] {
303+
// if thread pool queue has only one worker thread, can't dispatch tasks from within a task and then wait for them (deadlock)
304+
auto* tp = m_impl->thread_pool.thread_count() > 1 ? &m_impl->thread_pool : nullptr;
305+
auto func = [path = m_impl->load.request.path, gfx = m_impl->window.gfx, status = &m_impl->load.request.status, tp] {
288306
auto scene = Scene{gfx};
289-
if (!load_gltf(scene, path.c_str(), status)) { logger::error("[Engine] Failed to load GLTF: [{}]", path); }
307+
if (!load_gltf(scene, path.c_str(), *status, tp)) { logger::error("[Engine] Failed to load GLTF: [{}]", path); }
290308
// return the scene even on failure, it will be empty but valid
291309
return scene;
292310
};
293311
// store future
294-
m_impl->load.request.future = std::async(std::launch::async, func);
312+
m_impl->load.request.future = m_impl->thread_pool.enqueue(func);
295313
return true;
296314
}
297315

@@ -301,6 +319,8 @@ LoadStatus Engine::load_status() const {
301319
return {.stage = status.stage.load(), .total = status.total, .done = status.done};
302320
}
303321

322+
std::size_t Engine::load_thread_count() const { return m_impl->thread_pool.thread_count(); }
323+
304324
Scene& Engine::scene() const { return m_impl->scene; }
305325
GLFWwindow* Engine::window() const { return m_impl->window.window.get(); }
306326
Glfw::State const& Engine::state() const { return m_impl->window.window.get().state(); }

lib/render/src/renderer.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,10 @@ bool Renderer::next_frame(std::span<vk::CommandBuffer> out) {
179179

180180
// acquire swapchain image
181181
auto acquired = ImageView{};
182-
if (m_impl->swapchain.acquire(m_impl->window.framebuffer_extent(), acquired, *frame.sync.draw) != vk::Result::eSuccess) { return false; }
182+
{
183+
auto lock = std::scoped_lock{m_impl->gfx.shared->mutex};
184+
if (m_impl->swapchain.acquire(lock, m_impl->window.framebuffer_extent(), acquired, *frame.sync.draw) != vk::Result::eSuccess) { return false; }
185+
}
183186
m_impl->gfx.reset(*frame.sync.drawn);
184187
m_impl->gfx.shared->defer_queue.next();
185188

lib/scene/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ endif()
3535
target_sources(${PROJECT_NAME} PRIVATE
3636
include/${target_prefix}/scene/camera.hpp
3737
include/${target_prefix}/scene/fly_cam.hpp
38+
include/${target_prefix}/scene/gltf_loader.hpp
3839
include/${target_prefix}/scene/id.hpp
3940
include/${target_prefix}/scene/lights.hpp
4041
include/${target_prefix}/scene/load_status.hpp
@@ -48,6 +49,7 @@ target_sources(${PROJECT_NAME} PRIVATE
4849
src/detail/gltf.hpp
4950

5051
src/camera.cpp
52+
src/gltf_loader.cpp
5153
src/material.cpp
5254
src/scene.cpp
5355
src/transform.cpp
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#include <djson/json.hpp>
2+
#include <facade/scene/load_status.hpp>
3+
#include <facade/scene/scene.hpp>
4+
#include <atomic>
5+
6+
namespace facade {
7+
class ThreadPool;
8+
9+
///
10+
/// \brief Concurrent status for multi-threaded loading
11+
///
12+
struct AtomicLoadStatus {
13+
std::atomic<LoadStage> stage{};
14+
std::atomic<std::size_t> total{};
15+
std::atomic<std::size_t> done{};
16+
17+
void reset() {
18+
stage = LoadStage::eNone;
19+
total = {};
20+
done = {};
21+
}
22+
};
23+
24+
class Scene::GltfLoader {
25+
public:
26+
///
27+
/// \brief Construct a GltfLoader.
28+
/// \param out_scene The scene to load into
29+
/// \param out_status AtomicLoadStatus to be updated as the scene is loaded
30+
///
31+
GltfLoader(Scene& out_scene, AtomicLoadStatus& out_status) : m_scene(out_scene), m_status(out_status) { m_status.reset(); }
32+
33+
///
34+
/// \brief Load data from a GLTF file.
35+
/// \param json The root JSON node for the GLTF asset
36+
/// \param provider Data provider with the JSON parent directory mounted
37+
/// \param thread_pool Optional pointer to thread pool to use for async loading of images and buffers
38+
/// \returns true If successfully loaded
39+
///
40+
/// If the GLTF data fails to load, the scene data will remain unchanged.
41+
/// This function purposely throws on fatal errors.
42+
///
43+
bool operator()(dj::Json const& json, DataProvider const& provider, ThreadPool* thread_pool = {}) noexcept(false);
44+
///
45+
/// \brief Obtain the load status.
46+
///
47+
AtomicLoadStatus const& load_status() const { return m_status; }
48+
49+
private:
50+
Scene& m_scene;
51+
AtomicLoadStatus& m_status;
52+
};
53+
} // namespace facade

lib/scene/include/facade/scene/load_status.hpp

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,15 @@ namespace facade {
99
enum class LoadStage : std::uint8_t {
1010
eNone,
1111
eParsingJson,
12-
eLoadingImages,
13-
eUploadingTextures,
14-
eUploadingMeshes,
12+
eUploadingResources,
1513
eBuildingScenes,
1614
eCOUNT_,
1715
};
1816

1917
///
2018
/// \brief String map for LoadStage.
2119
///
22-
constexpr auto load_stage_str = EnumArray<LoadStage, std::string_view>{
23-
"None", "Parsing JSON", "Loading Images", "Uploading Textures", "Uploading Meshes", "Building Scenes",
24-
};
20+
constexpr auto load_stage_str = EnumArray<LoadStage, std::string_view>{"None", "Parsing JSON", "Uploading Resources", "Building Scenes"};
2521
static_assert(std::size(load_stage_str.t) == static_cast<std::size_t>(LoadStage::eCOUNT_));
2622

2723
struct LoadStatus {

lib/scene/include/facade/scene/scene.hpp

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,23 @@
22
#include <facade/scene/camera.hpp>
33
#include <facade/scene/id.hpp>
44
#include <facade/scene/lights.hpp>
5-
#include <facade/scene/load_status.hpp>
65
#include <facade/scene/material.hpp>
76
#include <facade/scene/mesh.hpp>
87
#include <facade/scene/node.hpp>
98
#include <facade/scene/node_data.hpp>
10-
#include <facade/util/enum_array.hpp>
119
#include <facade/util/image.hpp>
1210
#include <facade/util/ptr.hpp>
1311
#include <facade/util/transform.hpp>
14-
#include <facade/vk/buffer.hpp>
1512
#include <facade/vk/pipeline.hpp>
1613
#include <facade/vk/static_mesh.hpp>
1714
#include <facade/vk/texture.hpp>
18-
#include <atomic>
1915
#include <memory>
20-
#include <optional>
2116
#include <span>
2217
#include <vector>
2318

24-
namespace dj {
25-
class Json;
26-
}
27-
2819
namespace facade {
2920
struct DataProvider;
3021

31-
///
32-
/// \brief Concurrent status for multi-threaded loading
33-
///
34-
struct AtomicLoadStatus {
35-
std::atomic<LoadStage> stage{};
36-
std::atomic<std::size_t> total{};
37-
std::atomic<std::size_t> done{};
38-
39-
void reset() {
40-
stage = LoadStage::eNone;
41-
total = {};
42-
done = {};
43-
}
44-
};
45-
4622
///
4723
/// \brief Immutable view of the resources stored in the scene.
4824
///
@@ -64,6 +40,8 @@ struct SceneResources {
6440
class Scene {
6541
public:
6642
using Resources = SceneResources;
43+
// defined in loader.hpp
44+
class GltfLoader;
6745

6846
///
6947
/// \brief Represents a single GTLF scene.
@@ -85,18 +63,6 @@ class Scene {
8563
///
8664
explicit Scene(Gfx const& gfx);
8765

88-
///
89-
/// \brief Load data from a GLTF file.
90-
/// \param root The root JSON node for the GLTF asset
91-
/// \param provider Data provider with the JSON parent directory mounted
92-
/// \param out_status Optional pointer to AtomicLoadStatus to be updated by the Scene
93-
/// \returns true If successfully loaded
94-
///
95-
/// If the GLTF data fails to load, the scene data will remain unchanged.
96-
/// This function purposely throws on fatal errors.
97-
///
98-
bool load_gltf(dj::Json const& root, DataProvider const& provider, AtomicLoadStatus* out_status = {}) noexcept(false);
99-
10066
///
10167
/// \brief Add a Camera.
10268
/// \param camera Camera instance to add

0 commit comments

Comments
 (0)