diff --git a/lib/engine/include/facade/engine/editor/common.hpp b/lib/engine/include/facade/engine/editor/common.hpp index 45790ff..88950c7 100644 --- a/lib/engine/include/facade/engine/editor/common.hpp +++ b/lib/engine/include/facade/engine/editor/common.hpp @@ -90,10 +90,22 @@ class MainMenu : public MenuBar { /// class Popup : public Openable { public: - explicit Popup(char const* id, int flags = {}); + explicit Popup(char const* id, int flags = {}) : Popup(id, false, flags) {} ~Popup(); + static void open(char const* id); static void close_current(); + + protected: + explicit Popup(char const* id, bool modal, int flags); +}; + +/// +/// \brief RAII Dear ImGui PopupModal +/// +class Modal : public Popup { + public: + explicit Modal(char const* id, int flags = {}) : Popup(id, true, flags) {} }; /// diff --git a/lib/engine/include/facade/engine/engine.hpp b/lib/engine/include/facade/engine/engine.hpp index 275aeb3..b96b2ca 100644 --- a/lib/engine/include/facade/engine/engine.hpp +++ b/lib/engine/include/facade/engine/engine.hpp @@ -2,6 +2,7 @@ #include #include #include +#include #include namespace facade { @@ -28,6 +29,7 @@ struct EngineCreateInfo { class Engine { public: using CreateInfo = EngineCreateInfo; + using State = Glfw::State; Engine(Engine&&) noexcept; Engine& operator=(Engine&&) noexcept; @@ -64,9 +66,9 @@ class Engine { /// bool running() const; /// - /// \brief Poll events and obtain delta time + /// \brief Poll events and obtain updated state /// - float poll(); + State const& poll(); /// /// \brief Render the scene /// @@ -77,14 +79,29 @@ class Engine { /// void request_stop(); + /// + /// \brief Load a GLTF scene asynchronously + /// + /// Subsequent requests will be rejected if one is in flight + /// + bool load_async(std::string gltf_json_path, UniqueTask on_loaded = {}); + /// + /// \brief Obtain status of in-flight async load request (if active) + /// + LoadStatus load_status() const; + + glm::uvec2 window_extent() const; + glm::uvec2 framebuffer_extent() const; + Scene& scene() const; - Gfx const& gfx() const; - Glfw::Window const& window() const; - Glfw::State const& state() const; + State const& state() const; Input const& input() const; Renderer& renderer() const; + GLFWwindow* window() const; private: + void update_load_request(); + struct Impl; inline static Impl const* s_instance{}; std::unique_ptr m_impl{}; diff --git a/lib/engine/src/editor/common.cpp b/lib/engine/src/editor/common.cpp index 5e643ed..6efa27a 100644 --- a/lib/engine/src/editor/common.cpp +++ b/lib/engine/src/editor/common.cpp @@ -37,12 +37,14 @@ MainMenu::~MainMenu() { if (m_open) { ImGui::EndMainMenuBar(); } } -Popup::Popup(char const* id, int flags) : Openable(ImGui::BeginPopup(id, flags)) {} +Popup::Popup(char const* id, bool modal, int flags) : Openable(modal ? ImGui::BeginPopupModal(id, {}, flags) : ImGui::BeginPopup(id, flags)) {} Popup::~Popup() { if (m_open) { ImGui::EndPopup(); } } +void Popup::open(char const* id) { ImGui::OpenPopup(id); } + void Popup::close_current() { ImGui::CloseCurrentPopup(); } Menu::Menu(NotClosed, char const* label, bool enabled) : Openable(ImGui::BeginMenu(label, enabled)) {} diff --git a/lib/engine/src/engine.cpp b/lib/engine/src/engine.cpp index 40e7f27..dfb08b8 100644 --- a/lib/engine/src/engine.cpp +++ b/lib/engine/src/engine.cpp @@ -1,18 +1,25 @@ #include #include #include +#include #include #include #include #include #include +#include #include +#include #include #include #include #include +#include +#include namespace facade { +namespace fs = std::filesystem; + namespace { static constexpr std::size_t command_buffers_v{1}; @@ -150,6 +157,34 @@ struct RenderWindow { : window(std::move(window)), vulkan(GlfwWsi{this->window}, validation), gfx(vulkan.gfx()), renderer(gfx, this->window, gui.get(), Renderer::CreateInfo{command_buffers_v, msaa}), gui(std::move(gui)) {} }; + +bool load_gltf(Scene& out_scene, char const* path, std::atomic* out_status) { + auto const provider = FileDataProvider::mount_parent_dir(path); + auto json = dj::Json::from_file(path); + return out_scene.load_gltf(json, provider, out_status); +} + +template +bool ready(std::future const& future) { + return future.valid() && future.wait_for(std::chrono::seconds{}) == std::future_status::ready; +} + +template +bool timeout(std::future const& future) { + return future.valid() && future.wait_for(std::chrono::seconds{}) == std::future_status::timeout; +} + +template +bool busy(std::future const& future) { + return future.valid() && future.wait_for(std::chrono::seconds{}) == std::future_status::deferred; +} + +struct LoadRequest { + std::string path{}; + std::future future{}; + std::atomic status{}; + float start_time{}; +}; } // namespace struct Engine::Impl { @@ -158,7 +193,13 @@ struct Engine::Impl { Scene scene; std::uint8_t msaa; - DeltaTime dt{}; + + std::mutex mutex{}; + + struct { + LoadRequest request{}; + UniqueTask callback{}; + } load{}; Impl(UniqueWin window, std::uint8_t msaa, bool validation) : window(std::move(window), std::make_unique(), msaa, validation), renderer(this->window.gfx), scene(this->window.gfx), msaa(msaa) { @@ -166,6 +207,7 @@ struct Engine::Impl { } ~Impl() { + load.request.future = {}; window.gfx.device.waitIdle(); s_instance = {}; } @@ -189,21 +231,25 @@ void Engine::add_shader(Shader shader) { m_impl->window.renderer.add_shader(std: void Engine::show(bool reset_dt) { glfwShowWindow(window()); - if (reset_dt) { m_impl->dt = {}; } + if (reset_dt) { m_impl->window.window.get().glfw->reset_dt(); } } void Engine::hide() { glfwHideWindow(window()); } bool Engine::running() const { return !glfwWindowShouldClose(window()); } -float Engine::poll() { - window().glfw->poll_events(); +auto Engine::poll() -> State const& { + // the code in this call locks the mutex, so it's not inlined here + update_load_request(); + // ImGui wants all widget calls within BeginFrame() / EndFrame(), so begin here m_impl->window.gui->new_frame(); - return m_impl->dt(); + m_impl->window.window.get().glfw->poll_events(); + return m_impl.get()->window.window.get().state(); } void Engine::render() { auto cb = vk::CommandBuffer{}; + // we skip rendering the scene if acquiring a swapchain image fails (unlikely) if (m_impl->window.renderer.next_frame({&cb, 1})) { m_impl->renderer.render(scene(), renderer(), cb); } m_impl->window.gui->end_frame(); m_impl->window.renderer.render(); @@ -211,10 +257,70 @@ void Engine::render() { void Engine::request_stop() { glfwSetWindowShouldClose(window(), GLFW_TRUE); } +glm::uvec2 Engine::window_extent() const { return m_impl->window.window.get().window_extent(); } +glm::uvec2 Engine::framebuffer_extent() const { return m_impl->window.window.get().framebuffer_extent(); } + +bool Engine::load_async(std::string gltf_json_path, UniqueTask on_loaded) { + if (!fs::is_regular_file(gltf_json_path)) { + // early return if file will fail to load anyway + logger::error("[Engine] Invalid GLTF JSON path: [{}]", gltf_json_path); + return false; + } + // shared state will need to be accessed, lock the mutex + auto lock = std::scoped_lock{m_impl->mutex}; + if (m_impl->load.request.future.valid()) { + // we don't support discarding in-flight requests + logger::warn("[Engine] Denied attempt to load_async when a load request is already in flight"); + return false; + } + + // ready to start loading + logger::info("[Engine] Loading GLTF [{}]...", State::to_filename(gltf_json_path)); + // populate load request + m_impl->load.callback = std::move(on_loaded); + m_impl->load.request.path = std::move(gltf_json_path); + m_impl->load.request.status.store(LoadStatus::eStartingThread); + m_impl->load.request.start_time = time::since_start(); + auto func = [path = m_impl->load.request.path, gfx = m_impl->window.gfx, status = &m_impl->load.request.status] { + auto scene = Scene{gfx}; + if (!load_gltf(scene, path.c_str(), status)) { logger::error("[Engine] Failed to load GLTF: [{}]", path); } + // return the scene even on failure, it will be empty but valid + return scene; + }; + // store future + m_impl->load.request.future = std::async(std::launch::async, func); + return true; +} + +LoadStatus Engine::load_status() const { + auto lock = std::scoped_lock{m_impl->mutex}; + return m_impl->load.request.status.load(); +} + Scene& Engine::scene() const { return m_impl->scene; } -Gfx const& Engine::gfx() const { return m_impl->window.gfx; } -Glfw::Window const& Engine::window() const { return m_impl->window.window; } -Glfw::State const& Engine::state() const { return window().state(); } +GLFWwindow* Engine::window() const { return m_impl->window.window.get(); } +Glfw::State const& Engine::state() const { return m_impl->window.window.get().state(); } Input const& Engine::input() const { return state().input; } Renderer& Engine::renderer() const { return m_impl->window.renderer; } + +void Engine::update_load_request() { + auto lock = std::unique_lock{m_impl->mutex}; + // early return if future isn't valid or is still busy + if (!ready(m_impl->load.request.future)) { return; } + + // transfer scene (under mutex lock) + m_impl->scene = m_impl->load.request.future.get(); + // reset load status + m_impl->load.request.status.store(LoadStatus::eNone); + // move out the path + auto path = std::move(m_impl->load.request.path); + // move out the callback + auto callback = std::move(m_impl->load.callback); + auto const duration = time::since_start() - m_impl->load.request.start_time; + // unlock mutex to prevent possible deadlock (eg callback calls load_gltf again) + lock.unlock(); + logger::info("...GLTF [{}] loaded in [{:.2f}s]", State::to_filename(path), duration); + // invoke callback + if (callback) { callback(); } +} } // namespace facade diff --git a/lib/glfw/include/facade/glfw/glfw.hpp b/lib/glfw/include/facade/glfw/glfw.hpp index 4a36e40..fa93dce 100644 --- a/lib/glfw/include/facade/glfw/glfw.hpp +++ b/lib/glfw/include/facade/glfw/glfw.hpp @@ -18,6 +18,7 @@ struct Glfw { std::vector vk_extensions() const; void poll_events(); + void reset_dt(); bool operator==(Glfw const&) const = default; }; @@ -27,6 +28,9 @@ using UniqueWin = Unique; struct Glfw::State { Input input{}; std::vector file_drops{}; + float dt{}; + + static std::string to_filename(std::string_view path); }; struct Glfw::Window { diff --git a/lib/glfw/src/glfw.cpp b/lib/glfw/src/glfw.cpp index 3bb4189..7df3829 100644 --- a/lib/glfw/src/glfw.cpp +++ b/lib/glfw/src/glfw.cpp @@ -1,14 +1,22 @@ #include #include +#include +#include #include #include namespace facade { +namespace fs = std::filesystem; + namespace { std::weak_ptr g_glfw{}; std::mutex g_mutex{}; -std::unordered_map g_states{}; +struct { + std::unordered_map states{}; + // polling / dt is shared across all windows + DeltaTime dt{}; +} g_states{}; std::shared_ptr get_or_make_glfw() { auto lock = std::scoped_lock{g_mutex}; @@ -35,14 +43,23 @@ constexpr Action to_action(int glfw_action) { } // namespace void Glfw::poll_events() { - for (auto& [_, state] : g_states) { + auto const dt = g_states.dt(); + for (auto& [_, state] : g_states.states) { state.input.keyboard.next_frame(); state.input.mouse.next_frame(); state.file_drops.clear(); + state.dt = dt; } glfwPollEvents(); } +void Glfw::reset_dt() { g_states.dt = {}; } + +std::string Glfw::State::to_filename(std::string_view path) { + if (auto const i = path.find_last_of('/'); i != std::string_view::npos) { path = path.substr(i + 1); } + return std::string{path}; +} + auto Glfw::Window::make() -> UniqueWin { auto ret = Window{}; ret.glfw = get_or_make_glfw(); @@ -52,13 +69,13 @@ auto Glfw::Window::make() -> UniqueWin { ret.win = glfwCreateWindow(1, 1, "[untitled]", nullptr, nullptr); if (!ret.win) { throw InitError{"GLFW window creation failed"}; } if (glfwRawMouseMotionSupported()) { glfwSetInputMode(ret, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); } - glfwSetKeyCallback(ret, [](GLFWwindow* w, int key, int, int action, int) { g_states[w].input.keyboard.on_key(key, to_action(action)); }); - glfwSetMouseButtonCallback(ret, [](GLFWwindow* w, int button, int action, int) { g_states[w].input.mouse.on_button(button, to_action(action)); }); - glfwSetCursorPosCallback(ret, [](GLFWwindow* w, double x, double y) { g_states[w].input.mouse.on_position(glm::tvec2{x, y}); }); - glfwSetScrollCallback(ret, [](GLFWwindow* w, double x, double y) { g_states[w].input.mouse.on_scroll(glm::tvec2{x, y}); }); + glfwSetKeyCallback(ret, [](GLFWwindow* w, int key, int, int action, int) { g_states.states[w].input.keyboard.on_key(key, to_action(action)); }); + glfwSetMouseButtonCallback(ret, [](GLFWwindow* w, int button, int action, int) { g_states.states[w].input.mouse.on_button(button, to_action(action)); }); + glfwSetCursorPosCallback(ret, [](GLFWwindow* w, double x, double y) { g_states.states[w].input.mouse.on_position(glm::tvec2{x, y}); }); + glfwSetScrollCallback(ret, [](GLFWwindow* w, double x, double y) { g_states.states[w].input.mouse.on_scroll(glm::tvec2{x, y}); }); glfwSetDropCallback(ret, [](GLFWwindow* w, int count, char const** paths) { - auto& file_drops = g_states[w].file_drops; - for (int i = 0; i < count; ++i) { file_drops.push_back(paths[i]); } + auto& file_drops = g_states.states[w].file_drops; + for (int i = 0; i < count; ++i) { file_drops.push_back(fs::absolute(paths[i]).generic_string()); } }); return ret; } @@ -70,7 +87,7 @@ void Glfw::Deleter::operator()(Glfw const& glfw) const { void Glfw::Deleter::operator()(Window const& window) const { glfwDestroyWindow(window.win); - g_states.erase(window.win); + g_states.states.erase(window.win); } std::vector Glfw::vk_extensions() const { @@ -95,6 +112,6 @@ glm::uvec2 Glfw::Window::framebuffer_extent() const { Glfw::State const& Glfw::Window::state() const { auto lock = std::scoped_lock{g_mutex}; - return g_states[win]; + return g_states.states[win]; } } // namespace facade diff --git a/lib/scene/CMakeLists.txt b/lib/scene/CMakeLists.txt index 0a0a2d3..25bd881 100644 --- a/lib/scene/CMakeLists.txt +++ b/lib/scene/CMakeLists.txt @@ -37,6 +37,7 @@ target_sources(${PROJECT_NAME} PRIVATE include/${target_prefix}/scene/fly_cam.hpp include/${target_prefix}/scene/id.hpp include/${target_prefix}/scene/lights.hpp + include/${target_prefix}/scene/load_status.hpp include/${target_prefix}/scene/material.hpp include/${target_prefix}/scene/node_data.hpp include/${target_prefix}/scene/node.hpp diff --git a/lib/scene/include/facade/scene/load_status.hpp b/lib/scene/include/facade/scene/load_status.hpp new file mode 100644 index 0000000..79c0f25 --- /dev/null +++ b/lib/scene/include/facade/scene/load_status.hpp @@ -0,0 +1,46 @@ +#pragma once +#include +#include + +namespace facade { +enum class LoadStatus : std::uint8_t { + eNone, + eStartingThread, + eParsingBuffers, + eParsingBufferViews, + eParsingAccessors, + eParsingCameras, + eParsingSamplers, + eLoadingImages, + eParsingTextures, + eParsingMeshes, + eParsingMaterials, + eBuildingGeometry, + eBuildingNodes, + eBuildingScenes, + eUploadingResources, + eCOUNT_, +}; + +constexpr auto load_status_str = EnumArray{ + "None", + "Starting Thread", + "Parsing Buffers", + "Parsing BufferViews", + "Parsing Accessors", + "Parsing Cameras", + "Parsing Samplers", + "Loading Images", + "Parsing Textures", + "Parsing Meshes", + "Parsing Materials", + "Building Geometry", + "Building Nodes", + "Building Scenes", + "Uploading Resources", +}; + +static_assert(std::size(load_status_str.t) == static_cast(LoadStatus::eCOUNT_)); + +constexpr float load_progress(LoadStatus const stage) { return static_cast(stage) / static_cast(LoadStatus::eCOUNT_); } +} // namespace facade diff --git a/lib/scene/include/facade/scene/scene.hpp b/lib/scene/include/facade/scene/scene.hpp index e9a557a..071cc2f 100644 --- a/lib/scene/include/facade/scene/scene.hpp +++ b/lib/scene/include/facade/scene/scene.hpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -12,7 +13,7 @@ #include #include #include -#include +#include #include #include #include @@ -36,28 +37,39 @@ struct DataProvider; class Scene { public: + struct Tree { + struct Data { + std::vector roots{}; + }; + + std::vector roots{}; + Id camera{}; + Id id{}; + }; + static constexpr auto id_v = Id{0}; explicit Scene(Gfx const& gfx); - bool load_gltf(dj::Json const& root, DataProvider const& provider) noexcept(false); - bool load_gltf(std::string_view path); + bool load_gltf(dj::Json const& root, DataProvider const& provider, std::atomic* out_status = {}) noexcept(false); Id add(Camera camera); Id add(Sampler sampler); Id add(std::unique_ptr material); - Id add(StaticMesh mesh); - Id add(Image image); + Id add(Geometry const& geometry); + Id add(Image::View image, Id sampler, ColourSpace colour_space = ColourSpace::eSrgb); Id add(Mesh mesh); Id add(Node node, Id parent); - Id id() const { return m_tree.id; } - std::size_t scene_count() const { return m_storage.data.trees.size(); } - bool load(Id id); + Id tree_id() const { return m_tree.id; } + std::size_t tree_count() const { return m_storage.data.trees.size(); } + bool load(Id id); Ptr find(Id id) const; Ptr find(Id id); Ptr find(Id id) const; + Ptr find(Id id) const; + Ptr find(Id id) const; Ptr find(Id id) const; std::span roots() { return m_tree.roots; } std::span roots() const { return m_tree.roots; } @@ -75,16 +87,6 @@ class Scene { private: struct TreeBuilder; - struct Tree { - struct Data { - std::vector roots{}; - }; - - std::vector roots{}; - Id camera{}; - Id id{}; - }; - struct Data { std::vector nodes{}; std::vector trees{}; @@ -95,16 +97,15 @@ class Scene { std::vector samplers{}; std::vector> materials{}; std::vector static_meshes{}; - std::vector images{}; std::vector textures{}; std::vector meshes{}; - std::vector instances{}; + Data data{}; Id next_node{}; }; void add_default_camera(); - bool load_tree(Id id); + bool load_tree(Id id); Id add_unchecked(Mesh mesh); Id add_unchecked(std::vector& out, Node&& node); static Node const* find_node(std::span nodes, Id id); diff --git a/lib/scene/src/detail/gltf.cpp b/lib/scene/src/detail/gltf.cpp index df6810c..362f57b 100644 --- a/lib/scene/src/detail/gltf.cpp +++ b/lib/scene/src/detail/gltf.cpp @@ -466,19 +466,27 @@ struct Data { m.double_sided = json["doubleSided"].as_bool(dj::Boolean{m.double_sided}).value; } - Storage parse(dj::Json const& scene) { + Storage parse(dj::Json const& scene, std::atomic& out_status) { storage = {}; + out_status = LoadStatus::eParsingBuffers; for (auto const& b : scene["buffers"].array_view()) { buffer(b); } + out_status = LoadStatus::eParsingBufferViews; for (auto const& bv : scene["bufferViews"].array_view()) { buffer_view(bv); } + out_status = LoadStatus::eParsingAccessors; for (auto const& s : scene["accessors"].array_view()) { accessor(s); } + out_status = LoadStatus::eParsingCameras; for (auto const& c : scene["cameras"].array_view()) { camera(c); } + out_status = LoadStatus::eParsingSamplers; for (auto const& s : scene["samplers"].array_view()) { sampler(s); } + out_status = LoadStatus::eLoadingImages; for (auto const& i : scene["images"].array_view()) { image(i); } + out_status = LoadStatus::eParsingTextures; for (auto const& t : scene["textures"].array_view()) { texture(t); } - for (auto const& m : scene["materials"].array_view()) { material(m); } - + out_status = LoadStatus::eParsingMeshes; for (auto const& m : scene["meshes"].array_view()) { mesh(m); } + out_status = LoadStatus::eParsingMaterials; + for (auto const& m : scene["materials"].array_view()) { material(m); } // Texture will use ColourSpace::sRGB by default; change non-colour textures to be linear auto set_linear = [this](std::size_t index) { storage.textures.at(index).colour_space = ColourSpace::eLinear; }; @@ -573,10 +581,12 @@ std::vector children(dj::Json const& json) { } } // namespace -Asset Asset::parse(dj::Json const& json, DataProvider const& provider) { +Asset Asset::parse(dj::Json const& json, DataProvider const& provider, std::atomic& out_status) { auto ret = Asset{}; - auto storage = Data::Parser{provider}.parse(json); + auto storage = Data::Parser{provider}.parse(json, out_status); if (storage.accessors.empty()) { return {}; } + + out_status = LoadStatus::eBuildingGeometry; ret.cameras = std::move(storage.cameras); ret.images = std::move(storage.images); ret.materials = std::move(storage.materials); @@ -593,6 +603,7 @@ Asset Asset::parse(dj::Json const& json, DataProvider const& provider) { } } + out_status = LoadStatus::eBuildingNodes; auto const& nodes = json["nodes"].array_view(); ret.nodes.reserve(nodes.size()); for (auto const& node : nodes) { @@ -608,6 +619,7 @@ Asset Asset::parse(dj::Json const& json, DataProvider const& provider) { ret.nodes.push_back(Node{node["name"].as(), transform(node), children(node["children"]), index, type}); } + out_status = LoadStatus::eBuildingScenes; auto const& scenes = json["scenes"].array_view(); ret.scenes.reserve(scenes.size()); for (auto const& scene : scenes) { diff --git a/lib/scene/src/detail/gltf.hpp b/lib/scene/src/detail/gltf.hpp index 4a1077e..c5f0a2d 100644 --- a/lib/scene/src/detail/gltf.hpp +++ b/lib/scene/src/detail/gltf.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include @@ -6,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -146,7 +148,7 @@ struct Asset { std::vector scenes{}; std::size_t start_scene{}; - static Asset parse(dj::Json const& json, DataProvider const& provider); + static Asset parse(dj::Json const& json, DataProvider const& provider, std::atomic& out_status); }; } // namespace gltf } // namespace facade diff --git a/lib/scene/src/scene.cpp b/lib/scene/src/scene.cpp index e1900ac..0137d19 100644 --- a/lib/scene/src/scene.cpp +++ b/lib/scene/src/scene.cpp @@ -72,10 +72,6 @@ Mesh to_mesh(gltf::Mesh const& mesh) { return ret; } -Texture to_texture(Gfx const& gfx, vk::Sampler sampler, Image const& image, gltf::Texture const& texture) { - return Texture{gfx, sampler, image.view(), Texture::CreateInfo{.mip_mapped = true, .colour_space = texture.colour_space}}; -} - struct Img1x1 { std::byte bytes[4]{}; @@ -129,7 +125,7 @@ struct Scene::TreeBuilder { if (set_cam) { camera = node_id; } } - Tree operator()(Tree::Data const& tree, Id id) { + Tree operator()(Tree::Data const& tree, Id id) { auto ret = Tree{.id = id}; for (auto const index : tree.roots) { assert(index < in_gnodes.size()); @@ -141,6 +137,7 @@ struct Scene::TreeBuilder { // add node with default camera assert(!out_scene.m_storage.cameras.empty()); auto node = Node{}; + node.name = "camera"; // TODO node.transform.set_position({0.0f, 0.0f, 5.0f}); node.attach(Id{0}); @@ -150,11 +147,17 @@ struct Scene::TreeBuilder { } }; -bool Scene::load_gltf(dj::Json const& root, DataProvider const& provider) noexcept(false) { - auto asset = gltf::Asset::parse(root, provider); - if (asset.geometries.empty() || asset.scenes.empty()) { return false; } +bool Scene::load_gltf(dj::Json const& root, DataProvider const& provider, std::atomic* out_status) noexcept(false) { + auto status = std::atomic{}; + if (!out_status) { out_status = &status; } + auto asset = gltf::Asset::parse(root, provider, *out_status); + if (asset.geometries.empty() || asset.scenes.empty()) { + *out_status = LoadStatus::eNone; + return false; + } if (asset.start_scene >= asset.scenes.size()) { throw Error{fmt::format("Invalid start scene: {}", asset.start_scene)}; } + *out_status = LoadStatus::eUploadingResources; m_storage = {}; if (asset.cameras.empty()) { add(Camera{.name = "default"}); @@ -162,10 +165,9 @@ bool Scene::load_gltf(dj::Json const& root, DataProvider const& provider) noexce for (auto gltf_camera : asset.cameras) { add(to_camera(std::move(gltf_camera))); } } - m_storage.images = std::move(asset.images); for (auto const& sampler : asset.samplers) { add(to_sampler(m_gfx, sampler)); } for (auto const& material : asset.materials) { add(to_material(material)); } - for (auto const& geometry : asset.geometries) { add(StaticMesh{m_gfx, geometry}); } + for (auto const& geometry : asset.geometries) { add(geometry); } for (auto const& mesh : asset.meshes) { add(to_mesh(mesh)); } auto get_sampler = [this](std::optional sampler_id) { @@ -173,13 +175,16 @@ bool Scene::load_gltf(dj::Json const& root, DataProvider const& provider) noexce return m_storage.samplers[*sampler_id].sampler(); }; for (auto const& texture : asset.textures) { - m_storage.textures.push_back(to_texture(m_gfx, get_sampler(texture.sampler), m_storage.images.at(texture.source), texture)); + auto const tci = Texture::CreateInfo{.mip_mapped = true, .colour_space = texture.colour_space}; + m_storage.textures.emplace_back(m_gfx, get_sampler(texture.sampler), asset.images.at(texture.source), tci); } m_storage.data.nodes = std::move(asset.nodes); for (auto& scene : asset.scenes) { m_storage.data.trees.push_back(Tree::Data{.roots = std::move(scene.root_nodes)}); } - return load(asset.start_scene); + auto const ret = load(asset.start_scene); + *out_status = LoadStatus::eNone; + return ret; } Scene::Scene(Gfx const& gfx) : m_gfx(gfx), m_sampler(gfx) { add_default_camera(); } @@ -202,16 +207,23 @@ Id Scene::add(std::unique_ptr material) { return id; } -Id Scene::add(StaticMesh mesh) { +Id Scene::add(Geometry const& geometry) { auto const id = m_storage.static_meshes.size(); - m_storage.static_meshes.push_back(std::move(mesh)); + m_storage.static_meshes.emplace_back(m_gfx, geometry); return id; } -Id Scene::add(Image image) { - auto const id = m_storage.images.size(); - m_storage.images.push_back(std::move(image)); - return id; +Id Scene::add(Image::View image, Id sampler_id, ColourSpace colour_space) { + auto sampler = [&] { + if (sampler_id >= m_storage.samplers.size()) { + logger::warn("[Scene] Invalid sampler id: [{}], using default", sampler_id.value()); + return default_sampler(); + } + return m_storage.samplers[sampler_id].sampler(); + }(); + auto const ret = m_storage.textures.size(); + m_storage.textures.emplace_back(m_gfx, sampler, image, Texture::CreateInfo{.colour_space = colour_space}); + return ret; } Id Scene::add(Mesh mesh) { @@ -226,8 +238,8 @@ Id Scene::add(Node node, Id parent) { throw Error{fmt::format("Scene {}: Invalid parent Node Id: {}", m_name, parent)}; } -bool Scene::load(Id id) { - if (id >= scene_count()) { return false; } +bool Scene::load(Id id) { + if (id >= tree_count()) { return false; } return load_tree(id); } @@ -235,7 +247,8 @@ Ptr Scene::find(Id id) { return const_cast(std::as_const(*thi Ptr Scene::find(Id id) const { return find_node(m_tree.roots, id); } Ptr Scene::find(Id id) const { return id >= m_storage.materials.size() ? nullptr : m_storage.materials[id].get(); } - +Ptr Scene::find(Id id) const { return id >= m_storage.static_meshes.size() ? nullptr : &m_storage.static_meshes[id]; } +Ptr Scene::find(Id id) const { return id >= m_storage.textures.size() ? nullptr : &m_storage.textures[id]; } Ptr Scene::find(Id id) const { return id >= m_storage.meshes.size() ? nullptr : &m_storage.meshes[id]; } bool Scene::select(Id id) { @@ -255,13 +268,14 @@ Node const& Scene::camera() const { Texture Scene::make_texture(Image::View image) const { return Texture{m_gfx, default_sampler(), image}; } void Scene::add_default_camera() { - m_storage.cameras.push_back({}); + m_storage.cameras.push_back(Camera{.name = "default"}); auto node = Node{}; + node.name = "camera"; node.attach>(0); m_tree.camera = add_unchecked(m_tree.roots, std::move(node)); } -bool Scene::load_tree(Id id) { +bool Scene::load_tree(Id id) { assert(id < m_storage.data.trees.size()); m_tree = TreeBuilder{*this, m_storage.data.nodes}(m_storage.data.trees[id], id); return true; diff --git a/lib/util/CMakeLists.txt b/lib/util/CMakeLists.txt index bc1a9d6..c31cc85 100644 --- a/lib/util/CMakeLists.txt +++ b/lib/util/CMakeLists.txt @@ -81,6 +81,7 @@ target_sources(${PROJECT_NAME} PRIVATE include/${target_prefix}/util/ring_buffer.hpp include/${target_prefix}/util/time.hpp include/${target_prefix}/util/type_id.hpp + include/${target_prefix}/util/unique_task.hpp include/${target_prefix}/util/unique.hpp include/${target_prefix}/util/visitor.hpp diff --git a/lib/util/include/facade/util/unique_task.hpp b/lib/util/include/facade/util/unique_task.hpp new file mode 100644 index 0000000..df8a260 --- /dev/null +++ b/lib/util/include/facade/util/unique_task.hpp @@ -0,0 +1,41 @@ +#pragma once +#include + +namespace facade { +template +class UniqueTask; + +/// +/// \brief Type erased move-only callable (discount std::move_only_function) +/// +template +class UniqueTask { + public: + UniqueTask() = default; + + template + requires(!std::same_as && std::is_invocable_r_v) + UniqueTask(T t) : m_func(std::make_unique>(std::move(t))) {} + + Ret operator()(Args... args) const { + assert(m_func); + return (*m_func)(args...); + } + + explicit operator bool() const { return m_func != nullptr; } + + private: + struct Base { + virtual ~Base() = default; + virtual void operator()() = 0; + }; + template + struct Func : Base { + F f; + Func(F&& f) : f(std::move(f)) {} + void operator()() final { f(); } + }; + + std::unique_ptr m_func{}; +}; +} // namespace facade diff --git a/src/main.cpp b/src/main.cpp index 61d06ee..29d2da2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,12 +26,6 @@ using namespace facade; namespace { -bool load_gltf(Scene& out, std::string_view path) { - auto const provider = FileDataProvider::mount_parent_dir(path); - auto json = dj::Json::from_file(path.data()); - return out.load_gltf(json, provider); -} - static constexpr auto test_json_v = R"( { "scene": 0, @@ -221,21 +215,20 @@ void run() { auto material_id = Id{}; auto node_id = Id{}; - auto post_scene_load = [&] { - engine->scene().camera().transform.set_position({0.0f, 0.0f, 5.0f}); + auto post_scene_load = [&](Scene& scene) { + scene.camera().transform.set_position({0.0f, 0.0f, 5.0f}); auto material = std::make_unique(); material->albedo = {1.0f, 0.0f, 0.0f}; - material_id = engine->scene().add(std::move(material)); - auto static_mesh_id = engine->scene().add(StaticMesh{engine->gfx(), make_cubed_sphere(1.0f, 32)}); - auto mesh_id = engine->scene().add(Mesh{.primitives = {Mesh::Primitive{static_mesh_id, material_id}}}); + material_id = scene.add(std::move(material)); + auto static_mesh_id = scene.add(make_cubed_sphere(1.0f, 32)); + auto mesh_id = scene.add(Mesh{.primitives = {Mesh::Primitive{static_mesh_id, material_id}}}); auto node = Node{}; node.attach(mesh_id); node.instances.emplace_back().set_position({1.0f, -5.0f, -20.0f}); node.instances.emplace_back().set_position({-1.0f, 1.0f, 0.0f}); - node_id = engine->scene().add(std::move(node), 0); - engine->show(true); + node_id = scene.add(std::move(node), 0); }; auto init = [&] { @@ -247,11 +240,11 @@ void run() { engine->add_shader(lit); engine->add_shader(shaders::unlit()); - auto scene = Scene{engine->gfx()}; + auto& scene = engine->scene(); scene.dir_lights.push_back(DirLight{.direction = glm::normalize(glm::vec3{-1.0f, -1.0f, -1.0f}), .diffuse = glm::vec3{5.0f}}); scene.load_gltf(dj::Json::parse(test_json_v), DummyDataProvider{}); - engine->scene() = std::move(scene); - post_scene_load(); + post_scene_load(engine->scene()); + engine->show(true); }; init(); @@ -260,23 +253,31 @@ void run() { auto main_menu = MainMenu{}; + struct { + LoadStatus status{}; + std::string title{}; + } loading{}; + while (engine->running()) { - auto const dt = engine->poll(); - auto const& state = engine->state(); + auto const& state = engine->poll(); auto const& input = state.input; + auto const dt = state.dt; bool const mouse_look = input.mouse.held(GLFW_MOUSE_BUTTON_RIGHT); if (input.keyboard.pressed(GLFW_KEY_ESCAPE)) { engine->request_stop(); } glfwSetInputMode(engine->window(), GLFW_CURSOR, mouse_look ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL); if (!state.file_drops.empty()) { - auto load = [file = state.file_drops.front(), &engine] { - auto scene = Scene{engine->gfx()}; - if (!load_gltf(scene, file)) { logger::warn("Failed to load GLTF: [{}]", file); } - scene.dir_lights.push_back(DirLight{.direction = glm::normalize(glm::vec3{-1.0f, -1.0f, -1.0f}), .diffuse = glm::vec3{5.0f}}); - return scene; - }; - load(); + engine->load_async(state.file_drops.front(), [&] { post_scene_load(engine->scene()); }); + loading.title = fmt::format("Loading {}...", state.to_filename(state.file_drops.front())); + editor::Popup::open(loading.title.c_str()); + } + loading.status = engine->load_status(); + + if (auto popup = editor::Modal{loading.title.c_str()}) { + ImGui::Text("%s", load_status_str[loading.status].data()); + ImGui::ProgressBar(load_progress(loading.status), ImVec2{400.0f, 0}, load_status_str[loading.status].data()); + if (loading.status == LoadStatus::eNone) { editor::Popup::close_current(); } } auto& camera = engine->scene().camera();