diff --git a/.gitea/ISSUE_TEMPLATE/bug_report.md b/.gitea/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..b483bd5 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "" +labels: "" +assignees: "" +--- + +**Desktop (please complete the following information):** + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Something +2. ... + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.gitea/ISSUE_TEMPLATE/feature_request.md b/.gitea/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..328180c --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "" +labels: "" +assignees: "" +--- + +Relates to: + +- TODO + +## Context + +TODO + +## Implementation + +TODO + +## Additional resources + +TODO diff --git a/.gitea/pull_request_template.md b/.gitea/pull_request_template.md new file mode 100644 index 0000000..900c085 --- /dev/null +++ b/.gitea/pull_request_template.md @@ -0,0 +1,18 @@ +resolves TODO + +## Context + +TODO + +## Implementation + +TODO + +## Screenshots + +TODO + +## Testing Instructions + +1. Build engine via `bin/dev build && cd build/ && make -j4` +2. Run testbed via `./testbed` diff --git a/.gitignore b/.gitignore index aa505e8..02d09c9 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,6 @@ CMakeUserPresets.json *.bin *.out *.spv -*.spv.h # Runtime data pids diff --git a/.vscode/settings.json b/.vscode/settings.json index 9fc89bf..43db259 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -41,8 +41,8 @@ "LD_LIBRARY_PATH": "/home/caleb/.local/share/vulkan-sdk/1.4.357.1/x86_64/lib/VulkanLoader/lib:/home/caleb/.local/share/vulkan-sdk/1.4.357.1/x86_64/lib", "VK_ADD_LAYER_PATH": "/home/caleb/.local/share/vulkan-sdk/1.4.357.1/x86_64/share/vulkan/explicit_layer.d" }, - "cmake.buildDirectory": "${workspaceFolder}/build", - "cmake.generator": "Ninja", + "cmake.useCMakePresets": "always", + "cmake.copyCompileCommands": "${workspaceFolder}/build/compile_commands.json", "cmake.configureOnOpen": true, "files.associations": { ".clang-format": "yaml", diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..cc57b33 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,17 @@ +# Sophia + +## Dev helper (`bin/dev`) + +`bin/dev` wraps common project tasks. Run `bin/dev bash-completions` to list commands. + +### Creating issues + +Use `bin/dev issue` to create Gitea issues instead of calling the API or `tea` directly: + +```sh +bin/dev issue "" [-b "<body>"] [-l <label>]... [-a <user>]... [--open] +``` + +- Labels are repo label names (`bug`, `enhancement`, `question`, ...). +- The Gitea API cannot add an issue to a project. After creating an issue, tell the user its URL so they can add it to the "sophia" project (issue sidebar > Projects), or pass `--open`. +- Only create issues when the user asks; issues are visible on the remote. diff --git a/CMakeLists.txt b/CMakeLists.txt index 56431db..89400a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.22.2) +cmake_minimum_required(VERSION 3.25.2) set(NAME sophia) @@ -15,8 +15,6 @@ endif() if(CMAKE_BUILD_TYPE STREQUAL "Debug") message(STATUS "Debug build: enabling debug logging") -else() - add_compile_definitions(NDEBUG) endif() set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..c5e528c --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,66 @@ +{ + "version": 6, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "debug", + "displayName": "Debug", + "inherits": "base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "release", + "displayName": "Release (with debug info)", + "inherits": "base", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo" + } + }, + { + "name": "asan", + "displayName": "Debug + AddressSanitizer/UBSan", + "inherits": "debug", + "cacheVariables": { + "CMAKE_CXX_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer", + "CMAKE_C_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer" + } + }, + { + "name": "tsan", + "displayName": "Debug + ThreadSanitizer", + "inherits": "debug", + "cacheVariables": { + "CMAKE_CXX_FLAGS": "-fsanitize=thread", + "CMAKE_C_FLAGS": "-fsanitize=thread" + } + } + ], + "buildPresets": [ + { + "name": "debug", + "configurePreset": "debug" + }, + { + "name": "release", + "configurePreset": "release" + }, + { + "name": "asan", + "configurePreset": "asan" + }, + { + "name": "tsan", + "configurePreset": "tsan" + } + ] +} diff --git a/README.md b/README.md index 2637f3c..6e268ef 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,21 @@ COMING SOON ### Dependencies -- [cmake](https://cmake.org/) +- [cmake](https://cmake.org/) 3.25+ +- [Ninja](https://ninja-build.org/) - [Vulkan SDK](https://vulkan.lunarg.com/) - With validation layers ### Build instructions +Builds use the presets in `CMakePresets.json` (`debug`, `release`), each in its own `build/<preset>` directory: + ```sh -cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -cd build -make -j +cmake --preset debug +cmake --build --preset debug ``` +Or with the dev helper: `dev build [preset] [--fresh]`. + ## Third Party libraries used - [GLFW](https://github.com/glfw/glfw) - Window and input handling diff --git a/bin/dev b/bin/dev index b1dd9b3..f9425eb 100755 --- a/bin/dev +++ b/bin/dev @@ -19,6 +19,8 @@ WAIT_FOR_PROCESS = "wait_for_process" class DevHelper: SHADER_EXTENSIONS = (".vert", ".frag", ".comp", ".geom", ".tesc", ".tese") + PROJECT_NAME = "sophia" + DEFAULT_PRESET = "debug" def __init__(self): self.last_exit_status = 0 @@ -26,6 +28,9 @@ class DevHelper: # Core logic def call(self, *args, **kwargs): if not args: + print("Usage: dev <command> [args...]\n\nCommands:") + for command in self._commands(): + print(f" {command}") return command, rest = args[0], args[1:] @@ -35,9 +40,29 @@ class DevHelper: return method(*rest, **kwargs) # Primary command wrappers + + ## + # Configures and builds a CMake preset (see CMakePresets.json). + # dev build [preset] [--fresh] + # The preset defaults to debug and builds into build/<preset>. --fresh + # discards the preset's CMake cache before configuring. def build(self, *args, **kwargs): - command = f"rm -rf build && cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug" - self._exec_in_root(command, replace=True) + preset, fresh = self._parse_preset_args(args, "build") + self._build_preset(preset, fresh) + + ## + # Builds the debug preset (incrementally), then runs the testbed from + # build/debug. Any arguments are passed through to the testbed. + # dev testbed [args...] + def testbed(self, *args, **kwargs): + preset = self.DEFAULT_PRESET + self._build_preset(preset, fresh=False) + + preset_dir = os.path.join(self._project_root(), "build", preset) + binary = os.path.join(preset_dir, "bin", "testbed") + print(f"Running: {shlex.join([binary, *args])}") + os.chdir(preset_dir) + os.execv(binary, [binary, *args]) ## # Checks out a branch named after a Gitea issue URL. @@ -61,6 +86,40 @@ class DevHelper: flag = "" if exists else "-b " self._exec_in_root(f"git checkout {flag}{shlex.quote(branch_name)}", replace=True) + ## + # Creates an issue on this project's Gitea repo. + # dev issue <title> [-b|--body <text>] [-l|--label <name>]... [-a|--assignee <user>]... [-o|--open] + # The body is read from stdin if --body is not given and stdin is piped. + # Gitea's API cannot add issues to a project (only the web UI can), so the + # issue URL is printed and --open opens it in the browser to do so. + def issue(self, *args, **kwargs): + title, body, labels, assignees, open_in_browser = self._parse_issue_args(args) + + repo = self._origin_repo() + if repo is None: + raise RuntimeError("could not determine the Gitea repo from the origin remote") + + payload = {"title": title, "body": body} + if labels: + label_ids = self._fetch_label_ids(repo) + unknown = [label for label in labels if label not in label_ids] + if unknown: + raise ValueError(f"Unknown label(s): {', '.join(unknown)} (available: {', '.join(sorted(label_ids))})") + payload["labels"] = [label_ids[label] for label in labels] + if assignees: + payload["assignees"] = assignees + + issue = self._tea_api(f"repos/{repo}/issues", payload) + print(f"Created issue #{issue['number']}: {issue['title']}") + print(issue["html_url"]) + print(f"Add it to the project from the issue sidebar: Projects > {self.PROJECT_NAME}") + + if open_in_browser: + opener = shutil.which("xdg-open") or shutil.which("open") + if opener is None: + raise RuntimeError("xdg-open not found on PATH") + subprocess.Popen([opener, issue["html_url"]], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + def renderdoc(self, *args, **kwargs): qrenderdoc = self._find_qrenderdoc() self._exec_in_root(qrenderdoc, replace=True) @@ -101,14 +160,62 @@ class DevHelper: raise NotImplementedError(f"Not implemented for platform {sys.platform}") def bash_completions(self, *args, **kwargs): - completions = sorted( + print(" ".join(self._commands())) + + # Private helpers + def _commands(self): + return sorted( METHOD_TO_COMMAND.get(name, name) for name in vars(DevHelper) if not name.startswith("_") and name != "call" and callable(getattr(DevHelper, name)) ) - print(" ".join(completions)) - # Private helpers + def _parse_preset_args(self, args, command): + preset = None + fresh = False + for arg in args: + if arg == "--fresh": + fresh = True + elif arg.startswith("-"): + raise ValueError(f"Unknown option: {arg}") + elif preset is None: + preset = arg + else: + raise ValueError(f"Usage: dev {command} [preset] [--fresh]") + + preset = preset or self.DEFAULT_PRESET + presets = self._configure_presets() + if preset not in presets: + raise ValueError(f"Unknown preset: {preset} (available: {', '.join(presets)})") + return preset, fresh + + def _configure_presets(self): + with open(os.path.join(self._project_root(), "CMakePresets.json")) as f: + presets = json.load(f)["configurePresets"] + return [preset["name"] for preset in presets if not preset.get("hidden")] + + def _build_preset(self, preset, fresh): + configure = ["cmake", "--preset", preset] + if fresh: + configure.append("--fresh") + self._run_in_root(configure) + self._copy_compile_commands(preset) + self._run_in_root(["cmake", "--build", "--preset", preset]) + + # Editor tooling reads build/compile_commands.json, so point it at the + # most recently configured preset. + def _copy_compile_commands(self, preset): + build_dir = os.path.join(self._project_root(), "build") + source = os.path.join(build_dir, preset, "compile_commands.json") + if os.path.isfile(source): + shutil.copyfile(source, os.path.join(build_dir, "compile_commands.json")) + + def _run_in_root(self, command): + print(f"Running: {shlex.join(command)}") + result = subprocess.run(command, cwd=self._project_root()) + if result.returncode != 0: + sys.exit(result.returncode) + def _parse_issue_url(self, url): match = re.match(r"^(https?://[^/]+)/([^/]+)/([^/]+)/issues/(\d+)", url) if match is None: @@ -126,6 +233,63 @@ class DevHelper: raise RuntimeError(f"failed to fetch issue title from {endpoint}") return json.loads(result.stdout)["title"] + def _parse_issue_args(self, args): + title = None + body = None + labels = [] + assignees = [] + open_in_browser = False + + args = list(args) + while args: + arg = args.pop(0) + if arg in ("-b", "--body", "-l", "--label", "-a", "--assignee"): + if not args: + raise ValueError(f"{arg} requires a value") + value = args.pop(0) + if arg in ("-b", "--body"): + body = value + elif arg in ("-l", "--label"): + labels.append(value) + else: + assignees.append(value) + elif arg in ("-o", "--open"): + open_in_browser = True + elif arg.startswith("-"): + raise ValueError(f"Unknown option: {arg}") + elif title is None: + title = arg + else: + title = f"{title} {arg}" + + if not title: + raise ValueError("Must provide an issue title. Usage: dev issue <title> [-b <body>] [-l <label>]... [-a <user>]... [-o]") + if body is None: + body = "" if sys.stdin.isatty() else sys.stdin.read() + return title, body, labels, assignees, open_in_browser + + def _tea_api(self, endpoint, payload=None): + if shutil.which("tea") is None: + raise RuntimeError("tea not found on PATH (install gitea tea and run `tea login add`)") + + command = ["tea", "api", endpoint] + if payload is not None: + command += ["-d", "@-"] + print(f"Running: {' '.join(command)}") + result = subprocess.run( + command, + input=json.dumps(payload) if payload is not None else None, + stdout=subprocess.PIPE, + text=True, + ) + if result.returncode != 0: + raise RuntimeError(f"tea api request failed: {endpoint}") + return json.loads(result.stdout) + + def _fetch_label_ids(self, repo): + labels = self._tea_api(f"repos/{repo}/labels?limit=100") + return {label["name"]: label["id"] for label in labels} + def _format_branch_name(self, issue_repo, issue_number, issue_title): formatted_title = re.sub(r"-+", "-", re.sub(r"[^a-z0-9\-]", "", re.sub(r"\s+", "-", issue_title.lower().strip()))) if issue_repo == self._origin_repo(): diff --git a/_Design/glossary.md b/docs/glossary.md similarity index 71% rename from _Design/glossary.md rename to docs/glossary.md index f69d60c..370cea5 100644 --- a/_Design/glossary.md +++ b/docs/glossary.md @@ -1,36 +1,56 @@ # Glossary -## AssetRegistry +## ResourceRegistry -An AssetRegistry owns GPU-backed assets on the caller's behalf and hands out -lightweight Handles instead of pointers or ownership. +A ResourceRegistry owns GPU-backed resources on the caller's behalf and hands out +lightweight Handles instead of pointers or ownership. It is engine-internal: +frontend code never sees it, and reaches it through a ResourceRegistryRef. -Internally it's a collection of per-type pools, each responsible for one -asset type: +Internally it's a collection of per-type pools, each a `Pool<T, Handle>` +(a generation-checked slot pool). A type that needs extra behaviour wraps its +pool in a thin class instead: ``` -AssetRegistry - ├── MeshPool → MeshHandle - ├── TexturePool → TextureHandle (planned) - └── MaterialPool → MaterialHandle (planned) +ResourceRegistry + ├── meshPool Pool<Mesh, MeshHandle> + ├── renderTargetPool RenderTargetPool → RenderTargetHandle + │ (wraps Pool; waits for the GPU on release, applies resizes) + ├── texturePool (planned) + └── materialPool (planned) ``` +Engine code (`Application`, `ImGuiRenderer`, `SceneRenderer`) uses the pools +directly, e.g. `resources.renderTargetPool.get(handle)`. + A Handle is a small (index, generation) pair. The index locates the slot in the pool; the generation is bumped when that slot is released, so a Handle kept around after release() is detected as stale instead of resolving to -someone else's asset (or freed memory). +someone else's resource (or freed memory). + +Think: "Where does the engine keep every GPU resource, and how does it look one +up by handle?" + +## ResourceRegistryRef + +A ResourceRegistryRef is the frontend's non-owning, cheap-to-copy view of the +ResourceRegistry, returned by `Application::resources()`. It exposes only what +frontend code needs and hands out no pools, so no Vulkan-facing types leak +through the public API: + +``` +create<T>(...) load<T>(path) release<T>(handle) +requestResize(target, w, h) getExtent(target) isAlive(handle) +``` Typical lifecycle: ``` -load<Mesh>(path) / loadMesh(...) → MeshHandle - │ - get<Mesh>(handle) → const Mesh& +load<Mesh>(path) / create<Mesh>(...) → MeshHandle │ release<Mesh>(handle) ``` -Think: "Where do I get or release a GPU asset by handle, without owning its +Think: "How do I create or release a GPU resource by handle, without owning its lifetime directly?" ## Buffer @@ -256,12 +276,14 @@ This is primarily a UI concept, rather than a GPU concept. A PanelRegistry owns Panels on the caller's behalf and hands out lightweight PanelHandles instead of pointers or ownership. -Internally it's a generation-checked slot pool, the same shape AssetRegistry's -per-type pools use: +Internally it wraps a `Pool<Panel, PanelHandle>` (the same generation-checked +slot pool ResourceRegistry uses, in `sophia/util/pool.hpp`) and adds the +Panel-specific parts: lookup by type or name, and the default dock layout: ``` PanelRegistry - └── Slot { Panel, generation } → PanelHandle + └── Pool<Panel, PanelHandle> + └── Slot { Panel, generation } → PanelHandle ``` Typical lifecycle: @@ -279,6 +301,34 @@ Each frame, update(frameInfo) calls onUpdate on every visible Panel it holds. Think: "Where do I register or look up a UI panel by handle, without owning its lifetime directly?" +## SceneRegistry + +A SceneRegistry owns Scenes on the caller's behalf and hands out lightweight +SceneHandles instead of pointers or ownership. Like PanelRegistry, it is owned +by Application and reached from frontend code directly (`scenes()`), rather than +through ResourceRegistryRef: scenes are CPU-side entity data, not GPU resources. + +Internally it wraps a `Pool<Scene, SceneHandle>`: + +``` +SceneRegistry + └── Pool<Scene, SceneHandle> + └── Slot { Scene, generation } → SceneHandle +``` + +Typical lifecycle: + +``` +create() → SceneHandle + │ + get(handle) → Scene& + │ + release(handle) +``` + +Think: "Where do I create or look up a Scene by handle, without owning its +lifetime directly?" + ## Sprite A Sprite is a 2D renderable representation of an image, usually a rectangular region of a texture. diff --git a/editor/main.cpp b/editor/main.cpp index bb66b1f..7e0eb49 100644 --- a/editor/main.cpp +++ b/editor/main.cpp @@ -18,7 +18,10 @@ class Editor : public sophia::Application { panels().emplace<SceneHierarchyPanel>(); panels().emplace<InspectorPanel>(); panels().emplace<AssetBrowserPanel>(); - panels().emplace<ViewportPanel>(); + sophia::RenderTargetHandle sceneTarget = + resources().create<sophia::RenderTarget>(); + views().create({.scene = scenes().create(), .target = sceneTarget}); + panels().emplace<ViewportPanel>(ui(), sceneTarget); panels().setDefaultLayout( {.left = {"Scene Hierarchy"}, diff --git a/editor/panels/viewport_panel.cpp b/editor/panels/viewport_panel.cpp index ef14bed..74ec7b8 100644 --- a/editor/panels/viewport_panel.cpp +++ b/editor/panels/viewport_panel.cpp @@ -24,8 +24,9 @@ void ViewportPanel::onUpdate(const sophia::FrameInfo&) { const ImVec2 size = ImGui::GetContentRegionAvail(); const ImVec2 end(origin.x + size.x, origin.y + size.y); + ui.image(target); + ImDrawList* drawList = ImGui::GetWindowDrawList(); - drawList->AddRectFilled(origin, end, IM_COL32(24, 26, 32, 255)); if (showGrid) { constexpr float GRID_STEP = 40.0f; diff --git a/editor/panels/viewport_panel.hpp b/editor/panels/viewport_panel.hpp index b68205b..d470529 100644 --- a/editor/panels/viewport_panel.hpp +++ b/editor/panels/viewport_panel.hpp @@ -1,15 +1,25 @@ #pragma once #include <sophia/frame_info.hpp> +#include <sophia/handles.hpp> #include <sophia/ui/panel.hpp> +#include <sophia/ui/ui.hpp> class ViewportPanel : public sophia::Panel { public: - ViewportPanel() : Panel{{.name = "Viewport"}} {} + ViewportPanel(sophia::UI& ui, sophia::RenderTargetHandle target) + : Panel{{.name = "Viewport", + .windowFlags = ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoScrollWithMouse}}, + ui{ui}, + target{target} {} void onUpdate(const sophia::FrameInfo& frameInfo) override; private: + sophia::UI& ui; + sophia::RenderTargetHandle target; + enum class GizmoMode { Move, Rotate, Scale }; GizmoMode gizmoMode = GizmoMode::Move; diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 41f862c..abb899f 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -17,6 +17,11 @@ target_include_directories(${ENGINE_NAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" ) +target_compile_definitions(${ENGINE_NAME} PUBLIC + GLM_FORCE_RADIANS + GLM_FORCE_DEPTH_ZERO_TO_ONE +) + target_link_libraries(${ENGINE_NAME} PUBLIC vulkan shaderc diff --git a/engine/include/sophia/application.hpp b/engine/include/sophia/application.hpp index 4870fcb..a1e5c6c 100644 --- a/engine/include/sophia/application.hpp +++ b/engine/include/sophia/application.hpp @@ -3,21 +3,21 @@ #include <cstdlib> #include <iostream> #include <memory> -#include <sophia/asset_registry.hpp> #include <sophia/camera_2d.hpp> #include <sophia/canvas.hpp> #include <sophia/console/console_command_registry.hpp> #include <sophia/frame_info.hpp> +#include <sophia/math.hpp> +#include <sophia/resource_registry_ref.hpp> +#include <sophia/scene_registry.hpp> +#include <sophia/scene_view_registry.hpp> #include <sophia/types.hpp> #include <sophia/ui/debug_ui.hpp> #include <sophia/ui/panel_registry.hpp> +#include <sophia/ui/ui.hpp> #include <string> #include <vector> -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include <glm/glm.hpp> - namespace sophia { class Application { @@ -44,8 +44,11 @@ class Application { virtual void onRender() {} virtual void onDestroy() {} - AssetRegistry& assets() { return this->assetRegistry; } + ResourceRegistryRef resources(); PanelRegistry& panels() { return this->panelRegistry; } + SceneRegistry& scenes() { return this->sceneRegistry; } + SceneViewRegistry& views() { return this->sceneViewRegistry; } + UI& ui() { return this->_ui; } DebugUI& debugUI() { return this->_debugUI; } @@ -66,8 +69,10 @@ class Application { class Impl; std::unique_ptr<Impl> impl; - AssetRegistry assetRegistry; PanelRegistry panelRegistry; + SceneRegistry sceneRegistry; + SceneViewRegistry sceneViewRegistry; + UI _ui; DebugUI _debugUI; }; diff --git a/engine/include/sophia/asset_registry.hpp b/engine/include/sophia/asset_registry.hpp deleted file mode 100644 index 4ccf294..0000000 --- a/engine/include/sophia/asset_registry.hpp +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once - -#include <memory> -#include <sophia/handles.hpp> -#include <string> -#include <vector> - -#include "sophia/canvas.hpp" -#include "sophia/types.hpp" - -namespace sophia { - -class Device; -class Mesh; - -template <typename T> -struct AssetTraits; - -template <> -struct AssetTraits<Mesh> { - using Handle = MeshHandle; -}; - -class AssetRegistry { - public: - ~AssetRegistry(); - - AssetRegistry(const AssetRegistry&) = delete; - AssetRegistry& operator=(const AssetRegistry&) = delete; - - template <typename T> - typename AssetTraits<T>::Handle load(const std::string& path); - - /** - * TEMPORARY: Remove once load<Mesh> can read real model files from disk. - */ - MeshHandle loadMesh(std::vector<CanvasVertex> vertices, - std::vector<u32> indices); - - template <typename T> - void release(typename AssetTraits<T>::Handle handle); - - private: - friend class Application; - - explicit AssetRegistry(Device& device); - - template <typename T> - const T& get(typename AssetTraits<T>::Handle handle) const; - - class Impl; - std::unique_ptr<Impl> impl; -}; - -template <> -MeshHandle AssetRegistry::load<Mesh>(const std::string& path); - -template <> -void AssetRegistry::release<Mesh>(MeshHandle handle); - -template <> -const Mesh& AssetRegistry::get<Mesh>(MeshHandle handle) const; - -} // namespace sophia diff --git a/engine/include/sophia/camera_2d.hpp b/engine/include/sophia/camera_2d.hpp index fed29ed..88a87f0 100644 --- a/engine/include/sophia/camera_2d.hpp +++ b/engine/include/sophia/camera_2d.hpp @@ -1,9 +1,7 @@ #pragma once -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> +#include <sophia/math.hpp> namespace sophia { diff --git a/engine/include/sophia/canvas.hpp b/engine/include/sophia/canvas.hpp index 54b6d84..f11b811 100644 --- a/engine/include/sophia/canvas.hpp +++ b/engine/include/sophia/canvas.hpp @@ -2,12 +2,9 @@ #include <memory> #include <sophia/frame_info.hpp> +#include <sophia/math.hpp> #include <vector> -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include <glm/glm.hpp> - namespace sophia { class Application; diff --git a/engine/include/sophia/components.hpp b/engine/include/sophia/components.hpp index bcebe53..9b39b76 100644 --- a/engine/include/sophia/components.hpp +++ b/engine/include/sophia/components.hpp @@ -1,18 +1,17 @@ #pragma once -#include <sophia/asset_registry.hpp> -#include <string> - -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> -#include <glm/gtc/quaternion.hpp> +#include <iostream> +#include <ostream> +#include <sophia/handles.hpp> +#include <sophia/math.hpp> +#include <string> namespace sophia { struct TagComponent { std::string tag; // entt::hashed_string ? + uint32_t mask = 0; }; struct TransformComponent { @@ -28,10 +27,56 @@ struct TransformComponent { glm::mat3 normalMatrix() const { return glm::mat3{glm::transpose(glm::inverse(mat4()))}; } + + friend std::ostream& operator<<(std::ostream& os, + const TransformComponent& transform) { + os << "TransformComponent {\n" + << " Position: (" << transform.position.x << ", " + << transform.position.y << ", " << transform.position.z << ")\n" + << " Rotation (w, x, y, z): (" << transform.rotation.w << ", " + << transform.rotation.x << ", " << transform.rotation.y << ", " + << transform.rotation.z << ")\n" + << " Scale: (" << transform.scale.x << ", " << transform.scale.y << ", " + << transform.scale.z << ")\n" + << "}"; + return os; + } }; struct MeshComponent { MeshHandle handle; }; +struct CameraComponent { + struct Orthographic { + float height = 10.f; + float nearPlane = -1.f; + float farPlane = 1.f; + }; + + struct Perspective { + float fov = glm::radians(60.f); + float nearPlane = 0.1f; + float farPlane = 100.f; + }; + + enum class Projection { Orthographic, Perspective }; + + Projection projection = Projection::Orthographic; + Orthographic ortho; + Perspective perspective; + + glm::mat4 getProjection(float aspect) const { + if (projection == Projection::Perspective) { + return glm::perspective(perspective.fov, aspect, perspective.nearPlane, + perspective.farPlane); + } + + float halfHeight = ortho.height / 2.f; + float halfWidth = halfHeight * aspect; + return glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight, + ortho.nearPlane, ortho.farPlane); + } +}; + } // namespace sophia diff --git a/engine/include/sophia/entity.hpp b/engine/include/sophia/entity.hpp new file mode 100644 index 0000000..0c3950d --- /dev/null +++ b/engine/include/sophia/entity.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include <entt/entt.hpp> +#include <sophia/scene.hpp> +#include <sophia/types.hpp> +#include <sophia/util/assert.hpp> + +namespace sophia { + +class Entity { + public: + Entity(const Entity&) = default; + Entity& operator=(const Entity&) = default; + + Entity() = default; + Entity(entt::entity handle, Scene* scene); + + template <typename T, typename... Args> + T& addComponent(Args&&... args) { + SOPHIA_ASSERT(!hasComponent<T>(), "Entity already has component!"); + T& component = + scene->registry.emplace<T>(handle, std::forward<Args>(args)...); + // scene->OnComponentAdded<T>(*this, component); + return component; + } + + template <typename T> + T& getComponent() { + SOPHIA_ASSERT(hasComponent<T>(), "Entity does not have component!"); + return scene->registry.get<T>(handle); + } + + template <typename T> + bool hasComponent() { + return scene->registry.all_of<T>(handle); + } + + template <typename T> + void removeComponent() { + SOPHIA_ASSERT(hasComponent<T>(), "Entity does not have component!"); + scene->registry.remove<T>(handle); + } + + operator bool() const { return handle != entt::null; } + operator entt::entity() const { return handle; } + operator u32() const { return (u32)handle; } + + // UUID GetUUID() { return GetComponent<IDComponent>().ID; } + // const std::string& GetName() { return GetComponent<TagComponent>().Tag; } + + bool operator==(const Entity& other) const { + return handle == other.handle && scene == other.scene; + } + + bool operator!=(const Entity& other) const { return !(*this == other); } + + private: + entt::entity handle{entt::null}; + Scene* scene = nullptr; +}; + +} // namespace sophia diff --git a/engine/include/sophia/frame_info.hpp b/engine/include/sophia/frame_info.hpp index b542555..78f92bb 100644 --- a/engine/include/sophia/frame_info.hpp +++ b/engine/include/sophia/frame_info.hpp @@ -1,12 +1,9 @@ #pragma once #include <sophia/camera_2d.hpp> +#include <sophia/math.hpp> #include <sophia/types.hpp> -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include <glm/glm.hpp> - namespace sophia { struct FrameInfo { diff --git a/engine/include/sophia/handles.hpp b/engine/include/sophia/handles.hpp index a668790..298b51b 100644 --- a/engine/include/sophia/handles.hpp +++ b/engine/include/sophia/handles.hpp @@ -6,7 +6,7 @@ namespace sophia { -template <typename Tag> +template <typename T> struct Handle { static constexpr u32 INVALID_INDEX = (1u << 20) - 1; @@ -24,10 +24,25 @@ struct Handle { [[nodiscard]] auto operator<=>(const Handle&) const = default; }; -struct MeshTag {}; -struct PanelTag {}; +struct InvalidHandle { + template <typename T> + [[nodiscard]] constexpr operator Handle<T>() const { + return Handle<T>::invalid(); + } +}; -using MeshHandle = Handle<MeshTag>; -using PanelHandle = Handle<PanelTag>; +inline constexpr InvalidHandle INVALID_HANDLE{}; + +class Mesh; +class Panel; +class RenderTarget; +class Scene; +struct SceneView; + +using MeshHandle = Handle<Mesh>; +using PanelHandle = Handle<Panel>; +using RenderTargetHandle = Handle<RenderTarget>; +using SceneHandle = Handle<Scene>; +using SceneViewHandle = Handle<SceneView>; } // namespace sophia diff --git a/engine/include/sophia/math.hpp b/engine/include/sophia/math.hpp new file mode 100644 index 0000000..1ecfa71 --- /dev/null +++ b/engine/include/sophia/math.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include <glm/glm.hpp> +#include <glm/gtc/quaternion.hpp> + +namespace sophia { + +using vec2 = glm::vec2; +using vec3 = glm::vec3; +using vec4 = glm::vec4; +using ivec2 = glm::ivec2; +using uvec2 = glm::uvec2; +using mat3 = glm::mat3; +using mat4 = glm::mat4; +using quat = glm::quat; + +} // namespace sophia diff --git a/engine/include/sophia/resource_registry_ref.hpp b/engine/include/sophia/resource_registry_ref.hpp new file mode 100644 index 0000000..9ea4200 --- /dev/null +++ b/engine/include/sophia/resource_registry_ref.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include <sophia/canvas.hpp> +#include <sophia/handles.hpp> +#include <sophia/math.hpp> +#include <sophia/types.hpp> +#include <string> +#include <type_traits> +#include <utility> +#include <vector> + +namespace sophia { + +class ResourceRegistry; + +class ResourceRegistryRef { + public: + template <typename T> + Handle<T> load(const std::string& path); + + template <typename T, typename... Args> + Handle<T> create(Args&&... args) { + return createResource(std::type_identity<T>{}, std::forward<Args>(args)...); + } + + void requestResize(RenderTargetHandle handle, u32 width, u32 height); + + glm::vec2 getExtent(RenderTargetHandle handle) const; + + bool isAlive(RenderTargetHandle handle) const; + + template <typename T> + void release(Handle<T> handle); + + private: + friend class Application; + + explicit ResourceRegistryRef(ResourceRegistry& registry) + : registry{®istry} {} + + MeshHandle createResource(std::type_identity<Mesh>, + std::vector<CanvasVertex> vertices, + std::vector<u32> indices = {}); + + RenderTargetHandle createResource(std::type_identity<RenderTarget>, + u32 width = 1, u32 height = 1); + + ResourceRegistry* registry; +}; + +template <> +MeshHandle ResourceRegistryRef::load<Mesh>(const std::string& path); + +template <> +void ResourceRegistryRef::release<Mesh>(MeshHandle handle); + +template <> +void ResourceRegistryRef::release<RenderTarget>(RenderTargetHandle handle); + +} // namespace sophia diff --git a/engine/include/sophia/scene.hpp b/engine/include/sophia/scene.hpp new file mode 100644 index 0000000..687b50a --- /dev/null +++ b/engine/include/sophia/scene.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include <entt/entt.hpp> +#include <string> + +namespace sophia { + +class Entity; +class SceneRenderer; // !TEMP remove this + +class Scene { + public: + Scene(); + ~Scene(); + + void createEntity(const std::string& name = {}); + void destroyEntity(Entity entity); + + // The camera a render request uses when it doesn't name one. Null until + // set. + entt::entity getActiveCamera() const { return activeCamera; } + void setActiveCamera(entt::entity camera) { activeCamera = camera; } + + private: + friend class Entity; + friend class SceneRenderer; // !TEMP remove this + + entt::registry registry; + entt::entity activeCamera = entt::null; +}; + +} // namespace sophia diff --git a/engine/include/sophia/scene_registry.hpp b/engine/include/sophia/scene_registry.hpp new file mode 100644 index 0000000..2403b79 --- /dev/null +++ b/engine/include/sophia/scene_registry.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include <sophia/handles.hpp> +#include <sophia/scene.hpp> +#include <sophia/util/pool.hpp> + +namespace sophia { + +class SceneRegistry { + public: + SceneRegistry(const SceneRegistry&) = delete; + SceneRegistry& operator=(const SceneRegistry&) = delete; + + SceneRegistry() = default; + ~SceneRegistry() = default; + + SceneHandle create() { return pool.create(); } + + const Scene& get(SceneHandle handle) const { return pool.get(handle); } + Scene& get(SceneHandle handle) { return pool.get(handle); } + + void release(SceneHandle handle) { pool.release(handle); } + bool isAlive(SceneHandle handle) const { return pool.isAlive(handle); } + + u32 size() const { return pool.size(); } + + private: + Pool<Scene> pool{"Scene"}; +}; + +} // namespace sophia diff --git a/engine/include/sophia/scene_view.hpp b/engine/include/sophia/scene_view.hpp new file mode 100644 index 0000000..7d83da9 --- /dev/null +++ b/engine/include/sophia/scene_view.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include <sophia/entity.hpp> +#include <sophia/handles.hpp> +#include <sophia/math.hpp> +#include <sophia/scene.hpp> + +namespace sophia { + +enum class ClearFlags : u8 { + None = 0, + Color = 1 << 0, + Depth = 1 << 1, + Stencil = 1 << 2, + All = Color | Depth | Stencil +}; + +struct SceneView { + SceneHandle scene = INVALID_HANDLE; + RenderTargetHandle target = INVALID_HANDLE; + Entity camera; + + vec4 viewport{0.0f, 0.0f, 1.0f, 1.0f}; + + vec4 clearColor{0.f, 0.f, 0.f, 1.f}; + ClearFlags clearFlags = ClearFlags::All; + + u32 priority = 0; // Lower priority renders first + u32 layerMask = 0xFFFFFFFF; + + bool enabled = true; // Disabled views are kept but not rendered +}; + +} // namespace sophia diff --git a/engine/include/sophia/scene_view_registry.hpp b/engine/include/sophia/scene_view_registry.hpp new file mode 100644 index 0000000..fc2ef54 --- /dev/null +++ b/engine/include/sophia/scene_view_registry.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include <algorithm> +#include <sophia/handles.hpp> +#include <sophia/scene_view.hpp> +#include <sophia/util/pool.hpp> +#include <utility> +#include <vector> + +namespace sophia { + +/** + * Every live SceneView. The application renders all enabled views each frame, + * in ascending priority order, before the UI. + */ +class SceneViewRegistry { + public: + SceneViewRegistry(const SceneViewRegistry&) = delete; + SceneViewRegistry& operator=(const SceneViewRegistry&) = delete; + + SceneViewRegistry() = default; + ~SceneViewRegistry() = default; + + SceneViewHandle create(const SceneView& view = {}) { + isRenderQueueDirty = true; + return pool.create(view); + } + + const SceneView& get(SceneViewHandle handle) const { + return pool.get(handle); + } + + SceneView& get(SceneViewHandle handle) { + isRenderQueueDirty = true; + return pool.get(handle); + } + + void release(SceneViewHandle handle) { + isRenderQueueDirty = true; + pool.release(handle); + } + + bool isAlive(SceneViewHandle handle) const { return pool.isAlive(handle); } + + u32 size() const { return pool.size(); } + + template <typename Fn> + void forEach(Fn&& fn) { + isRenderQueueDirty = true; + pool.forEach(std::forward<Fn>(fn)); + } + + template <typename Fn> + void forEach(Fn&& fn) const { + pool.forEach(std::forward<Fn>(fn)); + } + + const std::vector<SceneViewHandle>& GetRenderQueue() const { + if (isRenderQueueDirty) { + rebuildRenderQueue(); + } + return renderQueue; + } + + private: + void rebuildRenderQueue() const { + renderQueue.clear(); + pool.forEachWithHandle([&](SceneViewHandle handle, const SceneView& view) { + if (view.enabled) { + renderQueue.push_back(handle); + } + }); + + std::ranges::stable_sort(renderQueue, {}, [this](SceneViewHandle handle) { + return pool.get(handle).priority; + }); + isRenderQueueDirty = false; + } + + Pool<SceneView> pool{"SceneView"}; + + mutable std::vector<SceneViewHandle> renderQueue; + mutable bool isRenderQueueDirty = true; +}; + +} // namespace sophia diff --git a/engine/include/sophia/ui/debug_ui.hpp b/engine/include/sophia/ui/debug_ui.hpp index 0037196..22d27a9 100644 --- a/engine/include/sophia/ui/debug_ui.hpp +++ b/engine/include/sophia/ui/debug_ui.hpp @@ -17,9 +17,9 @@ class DebugUI { void show(); void hide(); - void showPanelRegistry(bool show = true); - void showFrameStats(bool show = true); - void showConsole(bool show = true); + void showPanelRegistry(bool show = false); + void showFrameStats(bool show = false); + void showConsole(bool show = false); private: PanelRegistry& registry; diff --git a/engine/include/sophia/ui/panel_registry.hpp b/engine/include/sophia/ui/panel_registry.hpp index 09b3b87..5836fee 100644 --- a/engine/include/sophia/ui/panel_registry.hpp +++ b/engine/include/sophia/ui/panel_registry.hpp @@ -6,6 +6,7 @@ #include <sophia/ui/dock_layout.hpp> #include <sophia/ui/panel.hpp> #include <sophia/util/assert.hpp> +#include <sophia/util/pool.hpp> #include <string_view> #include <typeinfo> #include <utility> @@ -21,17 +22,17 @@ class PanelRegistry { PanelRegistry() = default; ~PanelRegistry() = default; - PanelHandle add(std::unique_ptr<Panel> panel); + PanelHandle add(std::unique_ptr<Panel> panel) { + return pool.add(std::move(panel)); + } template <typename T, typename... Args> PanelHandle emplace(Args&&... args) { return add(std::make_unique<T>(std::forward<Args>(args)...)); } - const Panel& get(PanelHandle handle) const; - Panel& get(PanelHandle handle) { - return const_cast<Panel&>(std::as_const(*this).get(handle)); - } + const Panel& get(PanelHandle handle) const { return pool.get(handle); } + Panel& get(PanelHandle handle) { return pool.get(handle); } template <typename T> const T& get(PanelHandle handle) const { @@ -46,19 +47,17 @@ class PanelRegistry { return const_cast<T&>(std::as_const(*this).template get<T>(handle)); } - void remove(PanelHandle handle); - bool isValid(PanelHandle handle) const; + void remove(PanelHandle handle) { pool.release(handle); } + bool isValid(PanelHandle handle) const { return pool.isAlive(handle); } template <typename T> const T* find() const { - for (const Slot& slot : slots) { - if (slot.panel) { - if (const T* panel = dynamic_cast<const T*>(slot.panel.get())) { - return panel; - } - } - } - return nullptr; + const T* result = nullptr; + pool.findIf([&result](const Panel& panel) { + result = dynamic_cast<const T*>(&panel); + return result != nullptr; + }); + return result; } template <typename T> @@ -67,29 +66,22 @@ class PanelRegistry { } const Panel* find(std::string_view name) const { - for (const Slot& slot : slots) { - if (slot.panel && slot.panel->getName() == name) { - return slot.panel.get(); - } - } - return nullptr; + return pool.findIf( + [name](const Panel& panel) { return panel.getName() == name; }); } Panel* find(std::string_view name) { return const_cast<Panel*>(std::as_const(*this).find(name)); } - u32 size() const; + u32 size() const { return pool.size(); } u32 visibleCount() const; std::vector<std::string_view> names() const { std::vector<std::string_view> result; - result.reserve(slots.size()); - for (const Slot& slot : slots) { - if (slot.panel) { - result.push_back(slot.panel->getName()); - } - } + result.reserve(pool.size()); + pool.forEach( + [&result](const Panel& panel) { result.push_back(panel.getName()); }); return result; } @@ -101,13 +93,7 @@ class PanelRegistry { const DockLayout& getDefaultLayout() const { return defaultLayout; } private: - struct Slot { - std::unique_ptr<Panel> panel; - u32 generation = 0; - }; - - std::vector<Slot> slots; - std::vector<u32> freeList; + Pool<Panel> pool{"Panel"}; DockLayout defaultLayout; }; diff --git a/engine/include/sophia/ui/ui.hpp b/engine/include/sophia/ui/ui.hpp new file mode 100644 index 0000000..9db7074 --- /dev/null +++ b/engine/include/sophia/ui/ui.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include <imgui.h> + +#include <sophia/handles.hpp> +#include <sophia/resource_registry_ref.hpp> + +namespace sophia { + +class ImGuiRenderer; + +class UI { + public: + UI(const UI&) = delete; + UI& operator=(const UI&) = delete; + + /** + * Draws the target filling the space left in the current window, and keeps + * the target sized to match. The target catches up on the next frame, so the + * image may be a frame behind while the window is being resized. + */ + void image(RenderTargetHandle handle); + + /** + * Texture to hand to ImGui::Image() that shows the target's contents. + * Changes when the target is resized, so fetch it each frame rather than + * caching it. + */ + ImTextureID getTextureId(RenderTargetHandle handle); + + private: + friend class Application; + + explicit UI(ResourceRegistryRef resources) : resources{resources} {} + + ResourceRegistryRef resources; + ImGuiRenderer* renderer = nullptr; +}; + +} // namespace sophia diff --git a/engine/include/sophia/util/pool.hpp b/engine/include/sophia/util/pool.hpp new file mode 100644 index 0000000..6ab2416 --- /dev/null +++ b/engine/include/sophia/util/pool.hpp @@ -0,0 +1,131 @@ +#pragma once + +#include <memory> +#include <sophia/handles.hpp> +#include <sophia/types.hpp> +#include <sophia/util/assert.hpp> +#include <utility> +#include <vector> + +namespace sophia { + +template <typename T> +class Pool { + public: + explicit Pool(const char* name) : name{name} {} + + Pool(const Pool&) = delete; + Pool& operator=(const Pool&) = delete; + + template <typename... Args> + Handle<T> create(Args&&... args) { + return add(std::make_unique<T>(std::forward<Args>(args)...)); + } + + Handle<T> add(std::unique_ptr<T> item) { + if (!freeList.empty()) { + u32 index = freeList.back(); + freeList.pop_back(); + slots[index].item = std::move(item); + return Handle<T>{index, slots[index].generation}; + } + + slots.push_back(Slot{std::move(item), 0}); + return Handle<T>{static_cast<u32>(slots.size() - 1), 0}; + } + + T& get(Handle<T> handle) { + return const_cast<T&>(std::as_const(*this).get(handle)); + } + + const T& get(Handle<T> handle) const { return *validate(handle).item; } + + void release(Handle<T> handle) { + validate(handle); + + Slot& slot = slots[handle.index]; + slot.item.reset(); + slot.generation = (slot.generation + 1) & GENERATION_MASK; + freeList.push_back(handle.index); + } + + bool isAlive(Handle<T> handle) const { + return handle.isValid() && handle.index < slots.size() && + slots[handle.index].generation == handle.generation && + slots[handle.index].item != nullptr; + } + + u32 size() const { return static_cast<u32>(slots.size() - freeList.size()); } + + template <typename Fn> + void forEach(Fn&& fn) { + for (Slot& slot : slots) { + if (slot.item) { + fn(*slot.item); + } + } + } + + template <typename Fn> + void forEach(Fn&& fn) const { + for (const Slot& slot : slots) { + if (slot.item) { + fn(*slot.item); + } + } + } + + /** Calls `fn(handle, item)` for every live item. */ + template <typename Fn> + void forEachWithHandle(Fn&& fn) const { + for (u32 index = 0; index < slots.size(); index++) { + const Slot& slot = slots[index]; + if (slot.item) { + fn(Handle<T>{index, slot.generation}, *slot.item); + } + } + } + + template <typename Predicate> + T* findIf(Predicate&& predicate) { + return const_cast<T*>( + std::as_const(*this).findIf(std::forward<Predicate>(predicate))); + } + + template <typename Predicate> + const T* findIf(Predicate&& predicate) const { + for (const Slot& slot : slots) { + if (slot.item && predicate(*slot.item)) { + return slot.item.get(); + } + } + return nullptr; + } + + private: + static constexpr u32 GENERATION_MASK = (1u << 12) - 1; + + struct Slot { + std::unique_ptr<T> item; + u32 generation = 0; + }; + + const Slot& validate(Handle<T> handle) const { + SOPHIA_ASSERT(handle.isValid(), "invalid handle:", name); + SOPHIA_ASSERT(handle.index < slots.size(), name, + "handle index out of range:", handle.index); + + const Slot& slot = slots[handle.index]; + SOPHIA_ASSERT(slot.generation == handle.generation, "stale", name, + "handle (generation mismatch):", handle.index); + SOPHIA_ASSERT(slot.item != nullptr, name, + "handle refers to a released item:", handle.index); + return slot; + } + + const char* name; + std::vector<Slot> slots; + std::vector<u32> freeList; +}; + +} // namespace sophia diff --git a/engine/src/application.cpp b/engine/src/application.cpp index b838e7b..84ae53e 100644 --- a/engine/src/application.cpp +++ b/engine/src/application.cpp @@ -1,6 +1,11 @@ +#include <imgui.h> + +#include <algorithm> #include <array> #include <chrono> #include <cstddef> +#include <glm/glm.hpp> +#include <map> #include <sophia/application.hpp> #include <sophia/util/logger.hpp> #include <string> @@ -8,20 +13,18 @@ #include <vector> #include <vulkan/vulkan.hpp> -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include <imgui.h> - -#include <glm/glm.hpp> - -#include "buffer.hpp" -#include "device.hpp" -#include "imgui_renderer.hpp" -#include "mesh.hpp" -#include "pipelines/graphics_pipeline.hpp" -#include "render_context.hpp" -#include "shader.hpp" -#include "systems/model_render_system.hpp" +#include "rendering/imgui_renderer.hpp" +#include "rendering/model_render_system.hpp" +#include "rendering/render_context.hpp" +#include "rendering/render_graph.hpp" +#include "rendering/scene_renderer.hpp" +#include "resources/mesh.hpp" +#include "resources/render_target.hpp" +#include "resources/resource_registry.hpp" +#include "vulkan/buffer.hpp" +#include "vulkan/device.hpp" +#include "vulkan/pipelines/graphics_pipeline.hpp" +#include "vulkan/shader.hpp" #include "window.hpp" namespace sophia { @@ -41,6 +44,12 @@ struct PushConstants { std::vector<std::string> commandLineArgs; +constexpr ClearColor CLEAR_COLOR = {0.01f, 0.01f, 0.02f, 1.0f}; + +bool hasFlag(ClearFlags flags, ClearFlags flag) { + return (static_cast<u8>(flags) & static_cast<u8>(flag)) != 0; +} + } // namespace class Application::Impl { @@ -50,12 +59,17 @@ class Application::Impl { config{config}, window{config.width, config.height, config.name}, device{&window}, - renderContext{window, device} {} + renderContext{window, device}, + resourceRegistry{device, renderContext.getImageFormat()} {} void run() { ModelRenderSystem modelRenderSystem{device, renderContext}; + SceneRenderer sceneRenderer{this->application.sceneRegistry, + this->resourceRegistry}; ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(), - renderContext.getImageCount()}; + renderContext.getImageCount(), + this->resourceRegistry}; + this->application._ui.renderer = &imguiRenderer; this->application.consoleCommandRegistry.registerCommand( "reset_layout", "Restore the default panel layout", @@ -65,8 +79,17 @@ class Application::Impl { }); this->application.onCreate(); - imguiRenderer.setDefaultLayout( - this->application.panelRegistry.getDefaultLayout()); + + auto& meshPool = this->resourceRegistry.meshPool; + auto& renderTargetPool = this->resourceRegistry.renderTargetPool; + + PanelRegistry& panelRegistry = this->application.panelRegistry; + SceneRegistry& sceneRegistry = this->application.sceneRegistry; + SceneViewRegistry& sceneViewRegistry = this->application.sceneViewRegistry; + const SceneViewRegistry& sceneViewRegistryConst = + this->application.sceneViewRegistry; + + imguiRenderer.setDefaultLayout(panelRegistry.getDefaultLayout()); auto startTime = std::chrono::high_resolution_clock::now(); auto currentTime = startTime; @@ -83,6 +106,10 @@ class Application::Impl { double elapsedTime = std::chrono::duration<double>(currentTime - startTime).count(); + // Resizing waits for the GPU, so it has to happen before any of this + // frame's work (or ImGui's references to the targets) is recorded. + renderTargetPool.applyPendingResizes(); + vk::Extent2D extent = renderContext.getExtent(); FrameInfo frameInfo{frameIndex, elapsedTime, deltaTime, glm::vec2(static_cast<float>(extent.width), @@ -102,41 +129,9 @@ class Application::Impl { imguiRenderer.beginFrame(); - this->application.panelRegistry.update(frameInfo); + panelRegistry.update(frameInfo); this->application.onRender(); - renderContext.transitionCurrentImage( - commandBuffer, vk::ImageLayout::eColorAttachmentOptimal); - - vk::ClearValue clearValue{}; - clearValue.color = - vk::ClearColorValue{std::array<float, 4>{0.01f, 0.01f, 0.02f, 1.0f}}; - - vk::RenderingAttachmentInfo colorAttachment{}; - colorAttachment.imageView = renderContext.getCurrentImageView(); - colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; - colorAttachment.loadOp = vk::AttachmentLoadOp::eClear; - colorAttachment.storeOp = vk::AttachmentStoreOp::eStore; - colorAttachment.clearValue = clearValue; - - vk::RenderingInfo renderingInfo{}; - renderingInfo.renderArea = vk::Rect2D{{0, 0}, extent}; - renderingInfo.layerCount = 1; - renderingInfo.colorAttachmentCount = 1; - renderingInfo.pColorAttachments = &colorAttachment; - - commandBuffer.beginRendering(renderingInfo); - - vk::Viewport viewport{0.0f, - 0.0f, - static_cast<float>(extent.width), - static_cast<float>(extent.height), - 0.0f, - 1.0f}; - vk::Rect2D scissor{{0, 0}, extent}; - commandBuffer.setViewport(0, viewport); - commandBuffer.setScissor(0, scissor); - std::vector<ModelRenderSystem::MeshInstance> instances; instances.reserve(this->meshes.size() + this->drawCalls.size()); for (const std::unique_ptr<Mesh>& mesh : this->meshes) { @@ -144,16 +139,61 @@ class Application::Impl { } instances.insert(instances.end(), this->drawCalls.begin(), this->drawCalls.end()); - - modelRenderSystem.render(commandBuffer, instances, frameInfo); this->drawCalls.clear(); - imguiRenderer.endFrame(commandBuffer); + RenderGraph graph; + ImageRef backbuffer = + graph.importImage(renderContext.getCurrentImage(), Access::Present); - commandBuffer.endRendering(); + std::map<RenderTargetHandle, ImageRef> viewTargets; + for (SceneViewHandle handle : sceneViewRegistryConst.GetRenderQueue()) { + const SceneView& view = sceneViewRegistryConst.get(handle); + if (!sceneRegistry.isAlive(view.scene) || + !renderTargetPool.isAlive(view.target)) { + continue; + } - renderContext.transitionCurrentImage(commandBuffer, - vk::ImageLayout::ePresentSrcKHR); + auto [it, inserted] = viewTargets.try_emplace(view.target); + if (inserted) { + it->second = + graph.importImage(renderTargetPool.get(view.target).getImage(), + Access::SampledRead); + } + + const vec4& color = view.clearColor; + graph.addPass( + "scene view", + [&](RenderGraph::PassBuilder& pass) { + pass.writeColor(it->second, + hasFlag(view.clearFlags, ClearFlags::Color) + ? vk::AttachmentLoadOp::eClear + : vk::AttachmentLoadOp::eLoad, + {color.r, color.g, color.b, color.a}); + }, + [&sceneRenderer, &sceneViewRegistry, handle](vk::CommandBuffer) { + const SceneView& view = sceneViewRegistry.get(handle); + if (view.camera) { + sceneRenderer.render(view); + } + }); + } + + graph.addPass( + "main", + [&](RenderGraph::PassBuilder& pass) { + pass.writeColor(backbuffer, vk::AttachmentLoadOp::eClear, + CLEAR_COLOR); + for (const auto& [handle, target] : viewTargets) { + pass.sample(target); + } + }, + [&](vk::CommandBuffer commandBuffer) { + modelRenderSystem.render(commandBuffer, instances, frameInfo); + + imguiRenderer.endFrame(commandBuffer); + }); + + graph.execute(commandBuffer); renderContext.endFrame(); @@ -164,6 +204,8 @@ class Application::Impl { device.waitIdle(); + this->application._ui.renderer = nullptr; + auto endTime = std::chrono::high_resolution_clock::now(); double totalRuntime = std::chrono::duration<double>(endTime - startTime).count(); @@ -193,7 +235,7 @@ class Application::Impl { this->drawCalls.push_back({&mesh, transform}); } - Device& getDevice() { return device; } + ResourceRegistry& getResourceRegistry() { return resourceRegistry; } private: Application& application; @@ -201,6 +243,7 @@ class Application::Impl { Window window; Device device; RenderContext renderContext; + ResourceRegistry resourceRegistry; std::vector<std::unique_ptr<Mesh>> meshes; std::vector<ModelRenderSystem::MeshInstance> drawCalls; @@ -214,13 +257,17 @@ Application::Application(const Config& config) : impl{std::make_unique<Impl>(*this, config)}, canvas{this}, args{std::move(commandLineArgs)}, - assetRegistry{impl->getDevice()}, + _ui{ResourceRegistryRef{impl->getResourceRegistry()}}, _debugUI{this->panelRegistry, this->consoleCommandRegistry} {} Application::~Application() = default; void Application::run() { impl->run(); } +ResourceRegistryRef Application::resources() { + return ResourceRegistryRef{impl->getResourceRegistry()}; +} + void Application::pushMesh(std::vector<CanvasVertex> vertices, std::vector<u32> indices) { impl->pushMesh(std::move(vertices), std::move(indices)); @@ -229,7 +276,7 @@ void Application::pushMesh(std::vector<CanvasVertex> vertices, void Application::clearMeshes() { impl->clearMeshes(); } void Application::drawMesh(MeshHandle handle, const glm::mat4& transform) { - impl->drawMesh(assetRegistry.get<Mesh>(handle), transform); + impl->drawMesh(impl->getResourceRegistry().meshPool.get(handle), transform); } } // namespace sophia diff --git a/engine/src/assets/asset_registry.cpp b/engine/src/assets/asset_registry.cpp deleted file mode 100644 index 27fcdd9..0000000 --- a/engine/src/assets/asset_registry.cpp +++ /dev/null @@ -1,60 +0,0 @@ -#include <sophia/asset_registry.hpp> -#include <sophia/util/assert.hpp> - -#include "mesh.hpp" -#include "pools/mesh_pool.hpp" - -namespace sophia { - -class AssetRegistry::Impl { - public: - explicit Impl(Device& device) : meshPool{device} {} - - MeshPool meshPool; - // TexturePool texturePool; - // MaterialPool materialPool; -}; - -AssetRegistry::AssetRegistry(Device& device) - : impl{std::make_unique<Impl>(device)} {} - -AssetRegistry::~AssetRegistry() = default; - -template <> -MeshHandle AssetRegistry::load<Mesh>(const std::string& path) { - (void)path; - SOPHIA_ASSERT(false, - "Mesh::load(path) is not implemented yet - no model file " - "format is supported; build one via Mesh::Builder for now"); - return MeshHandle{}; -} - -template <typename T> -typename AssetTraits<T>::Handle load(const std::string&) { - static_assert(sizeof(T) == 0, - "load<T> is not implemented for this asset type"); -} - -template <> -void AssetRegistry::release<Mesh>(MeshHandle handle) { - impl->meshPool.release(handle); -} - -template <> -const Mesh& AssetRegistry::get<Mesh>(MeshHandle handle) const { - return impl->meshPool.get(handle); -} - -MeshHandle AssetRegistry::loadMesh(std::vector<CanvasVertex> vertices, - std::vector<u32> indices) { - Mesh::Builder builder{}; - builder.vertices.reserve(vertices.size()); - for (const CanvasVertex& vertex : vertices) { - builder.vertices.push_back({vertex.position, vertex.color}); - } - builder.indices = std::move(indices); - - return impl->meshPool.create(builder); -} - -} // namespace sophia diff --git a/engine/src/assets/pools/mesh_pool.cpp b/engine/src/assets/pools/mesh_pool.cpp deleted file mode 100644 index 2183ebe..0000000 --- a/engine/src/assets/pools/mesh_pool.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include "assets/pools/mesh_pool.hpp" - -#include <sophia/util/assert.hpp> - -namespace sophia { - -namespace { -constexpr u32 GENERATION_MASK = (1u << 12) - 1; -} // namespace - -MeshPool::MeshPool(Device& device) : device{device} {} - -MeshPool::~MeshPool() = default; - -MeshHandle MeshPool::create(const Mesh::Builder& builder) { - auto mesh = std::make_unique<Mesh>(device, builder); - - if (!freeList.empty()) { - u32 index = freeList.back(); - freeList.pop_back(); - slots[index].mesh = std::move(mesh); - return MeshHandle{index, slots[index].generation}; - } - - slots.push_back(Slot{std::move(mesh), 0}); - return MeshHandle{static_cast<u32>(slots.size() - 1), 0}; -} - -const Mesh& MeshPool::get(MeshHandle handle) const { - SOPHIA_ASSERT(handle.isValid(), "invalid MeshHandle"); - SOPHIA_ASSERT(handle.index < slots.size(), - "MeshHandle index out of range:", handle.index); - - const Slot& slot = slots[handle.index]; - SOPHIA_ASSERT(slot.generation == handle.generation, - "stale MeshHandle (generation mismatch):", handle.index); - SOPHIA_ASSERT(slot.mesh != nullptr, - "MeshHandle refers to a released mesh:", handle.index); - return *slot.mesh; -} - -void MeshPool::release(MeshHandle handle) { - SOPHIA_ASSERT(handle.isValid(), "invalid MeshHandle"); - SOPHIA_ASSERT(handle.index < slots.size(), - "MeshHandle index out of range:", handle.index); - - Slot& slot = slots[handle.index]; - SOPHIA_ASSERT(slot.generation == handle.generation, - "stale MeshHandle (generation mismatch):", handle.index); - SOPHIA_ASSERT(slot.mesh != nullptr, - "MeshHandle already released:", handle.index); - - slot.mesh.reset(); - slot.generation = (slot.generation + 1) & GENERATION_MASK; - freeList.push_back(handle.index); -} - -} // namespace sophia diff --git a/engine/src/assets/pools/mesh_pool.hpp b/engine/src/assets/pools/mesh_pool.hpp deleted file mode 100644 index 218f0bb..0000000 --- a/engine/src/assets/pools/mesh_pool.hpp +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include <memory> -#include <vector> - -#include "device.hpp" -#include "mesh.hpp" - -namespace sophia { - -class MeshPool { - public: - explicit MeshPool(Device& device); - ~MeshPool(); - - MeshPool(const MeshPool&) = delete; - MeshPool& operator=(const MeshPool&) = delete; - - MeshHandle create(const Mesh::Builder& builder); - const Mesh& get(MeshHandle handle) const; - void release(MeshHandle handle); - - private: - struct Slot { - std::unique_ptr<Mesh> mesh; - u32 generation = 0; - }; - - Device& device; - - std::vector<Slot> slots; - std::vector<u32> freeList; -}; - -} // namespace sophia diff --git a/engine/src/imgui_renderer.cpp b/engine/src/rendering/imgui_renderer.cpp similarity index 71% rename from engine/src/imgui_renderer.cpp rename to engine/src/rendering/imgui_renderer.cpp index 538c29c..d458938 100644 --- a/engine/src/imgui_renderer.cpp +++ b/engine/src/rendering/imgui_renderer.cpp @@ -1,10 +1,12 @@ -#include "imgui_renderer.hpp" +#include "rendering/imgui_renderer.hpp" #include <imgui.h> #include <imgui_impl_glfw.h> #include <imgui_impl_vulkan.h> #include <imgui_internal.h> +#include "resources/render_target.hpp" + namespace sophia { namespace { @@ -20,7 +22,8 @@ void dockAll(const std::vector<std::string>& windows, ImGuiID nodeId) { ImGuiRenderer::ImGuiRenderer(Window& window, Device& device, vk::Format colorAttachmentFormat, - u32 minImageCount) { + u32 minImageCount, ResourceRegistry& resources) + : resources{resources} { // 1. Construct ImGui context IMGUI_CHECKVERSION(); ImGui::CreateContext(); @@ -67,12 +70,17 @@ ImGuiRenderer::ImGuiRenderer(Window& window, Device& device, } ImGuiRenderer::~ImGuiRenderer() { + for (const auto& [index, texture] : textures) { + ImGui_ImplVulkan_RemoveTexture(texture.descriptorSet); + } ImGui_ImplVulkan_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); } void ImGuiRenderer::beginFrame() { + releaseStaleTextures(); + ImGui_ImplVulkan_NewFrame(); ImGui_ImplGlfw_NewFrame(); ImGui::NewFrame(); @@ -82,6 +90,46 @@ void ImGuiRenderer::beginFrame() { applyLayoutIfNeeded(dockspaceId, DOCKSPACE_FLAGS); } +ImTextureID ImGuiRenderer::getTextureId(RenderTargetHandle handle) { + vk::ImageView view = + resources.renderTargetPool.get(handle).getImage().getView(); + + auto it = textures.find(handle.index); + if (it != textures.end()) { + if (it->second.generation == handle.generation && it->second.view == view) { + return (ImTextureID)it->second.descriptorSet; + } + ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet); + textures.erase(it); + } + + // ImGui owns the sampler; it only needs the view and the layout the image is + // left in once the scene pass has finished. + VkDescriptorSet descriptorSet = ImGui_ImplVulkan_AddTexture( + static_cast<VkImageView>(view), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + textures[handle.index] = {handle.generation, view, descriptorSet}; + return (ImTextureID)descriptorSet; +} + +// Resizing and releasing a target both wait for the GPU before destroying its +// image, so nothing in flight can still be using a stale texture. +void ImGuiRenderer::releaseStaleTextures() { + for (auto it = textures.begin(); it != textures.end();) { + RenderTargetHandle handle{it->first, it->second.generation}; + RenderTargetPool& renderTargetPool = resources.renderTargetPool; + + bool stale = + !renderTargetPool.isAlive(handle) || + renderTargetPool.get(handle).getImage().getView() != it->second.view; + if (stale) { + ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet); + it = textures.erase(it); + } else { + ++it; + } + } +} + void ImGuiRenderer::setDefaultLayout(DockLayout layout) { defaultLayout = std::move(layout); seedIfUnsaved = true; diff --git a/engine/src/imgui_renderer.hpp b/engine/src/rendering/imgui_renderer.hpp similarity index 60% rename from engine/src/imgui_renderer.hpp rename to engine/src/rendering/imgui_renderer.hpp index 34765cb..5ee558a 100644 --- a/engine/src/imgui_renderer.hpp +++ b/engine/src/rendering/imgui_renderer.hpp @@ -2,10 +2,13 @@ #include <imgui.h> +#include <sophia/handles.hpp> #include <sophia/types.hpp> #include <sophia/ui/dock_layout.hpp> +#include <unordered_map> -#include "device.hpp" +#include "resources/resource_registry.hpp" +#include "vulkan/device.hpp" #include "window.hpp" namespace sophia { @@ -16,17 +19,32 @@ class ImGuiRenderer { ImGuiRenderer& operator=(const ImGuiRenderer&) = delete; ImGuiRenderer(Window& window, Device& device, - vk::Format colorAttachmentFormat, u32 minImageCount); + vk::Format colorAttachmentFormat, u32 minImageCount, + ResourceRegistry& resources); ~ImGuiRenderer(); void beginFrame(); void endFrame(vk::CommandBuffer commandBuffer); + ImTextureID getTextureId(RenderTargetHandle handle); + void setDefaultLayout(DockLayout layout); void resetLayout(); private: void applyLayoutIfNeeded(ImGuiID dockspaceId, ImGuiDockNodeFlags flags); + void releaseStaleTextures(); + + struct Texture { + u32 generation; + vk::ImageView view; + VkDescriptorSet descriptorSet; + }; + + ResourceRegistry& resources; + + // Keyed by RenderTargetHandle::index. + std::unordered_map<u32, Texture> textures; DockLayout defaultLayout; bool seedIfUnsaved = false; diff --git a/engine/src/systems/model_render_system.cpp b/engine/src/rendering/model_render_system.cpp similarity index 98% rename from engine/src/systems/model_render_system.cpp rename to engine/src/rendering/model_render_system.cpp index 699602c..a0a796f 100644 --- a/engine/src/systems/model_render_system.cpp +++ b/engine/src/rendering/model_render_system.cpp @@ -1,4 +1,4 @@ -#include "model_render_system.hpp" +#include "rendering/model_render_system.hpp" #include <sophia/util/assert.hpp> diff --git a/engine/src/systems/model_render_system.hpp b/engine/src/rendering/model_render_system.hpp similarity index 87% rename from engine/src/systems/model_render_system.hpp rename to engine/src/rendering/model_render_system.hpp index 93d70b8..b3d7470 100644 --- a/engine/src/systems/model_render_system.hpp +++ b/engine/src/rendering/model_render_system.hpp @@ -4,10 +4,10 @@ #include <sophia/frame_info.hpp> #include <span> -#include "device.hpp" -#include "mesh.hpp" -#include "pipelines/graphics_pipeline.hpp" -#include "render_context.hpp" +#include "rendering/render_context.hpp" +#include "resources/mesh.hpp" +#include "vulkan/device.hpp" +#include "vulkan/pipelines/graphics_pipeline.hpp" namespace sophia { diff --git a/engine/src/render_context.cpp b/engine/src/rendering/render_context.cpp similarity index 97% rename from engine/src/render_context.cpp rename to engine/src/rendering/render_context.cpp index 76296c6..e57ace9 100644 --- a/engine/src/render_context.cpp +++ b/engine/src/rendering/render_context.cpp @@ -1,10 +1,12 @@ -#include "render_context.hpp" +#include "rendering/render_context.hpp" #include <limits> #include <sophia/util/assert.hpp> #include <sophia/util/logger.hpp> #include <stdexcept> +#include "vulkan/access.hpp" + namespace sophia { RenderContext::RenderContext(Window& window, Device& device) @@ -80,8 +82,7 @@ void RenderContext::endFrame() { vk::Semaphore waitSemaphore = frame.imageAvailable.get(); vk::Semaphore signalSemaphore = this->renderFinishedSemaphores.at(this->currentImageIndex).get(); - vk::PipelineStageFlags waitStage = - vk::PipelineStageFlagBits::eColorAttachmentOutput; + vk::PipelineStageFlags waitStage = SWAPCHAIN_ACQUIRE_STAGE; vk::CommandBuffer commandBuffer = frame.commandBuffer; vk::SubmitInfo submitInfo{}; diff --git a/engine/src/render_context.hpp b/engine/src/rendering/render_context.hpp similarity index 74% rename from engine/src/render_context.hpp rename to engine/src/rendering/render_context.hpp index 34e7ac1..25001e1 100644 --- a/engine/src/render_context.hpp +++ b/engine/src/rendering/render_context.hpp @@ -6,8 +6,8 @@ #include <vector> #include <vulkan/vulkan.hpp> -#include "device.hpp" -#include "swapchain.hpp" +#include "vulkan/device.hpp" +#include "vulkan/swapchain.hpp" #include "window.hpp" namespace sophia { @@ -31,14 +31,8 @@ class RenderContext { vk::CommandBuffer beginFrame(); void endFrame(); - void transitionCurrentImage(vk::CommandBuffer commandBuffer, - vk::ImageLayout newLayout) const { - this->swapchain->getImage(static_cast<int>(this->currentImageIndex)) - .transitionLayout(commandBuffer, newLayout); - } - vk::ImageView getCurrentImageView() const { - return this->swapchain->getImageView( - static_cast<int>(this->currentImageIndex)); + Image& getCurrentImage() const { + return this->swapchain->getImage(static_cast<int>(this->currentImageIndex)); } vk::Extent2D getExtent() const { return this->swapchain->getExtent(); } diff --git a/engine/src/rendering/render_graph.cpp b/engine/src/rendering/render_graph.cpp new file mode 100644 index 0000000..b15d6b6 --- /dev/null +++ b/engine/src/rendering/render_graph.cpp @@ -0,0 +1,168 @@ +#include "rendering/render_graph.hpp" + +#include <sophia/util/assert.hpp> +#include <utility> + +#include "vulkan/debug_label.hpp" + +namespace sophia { + +ImageRef RenderGraph::importImage(Image& image, Access finalAccess) { + ImageRef ref{static_cast<u32>(this->images.size())}; + this->images.push_back({&image, finalAccess, image.getAccessState()}); + return ref; +} + +void RenderGraph::addPass(std::string name, const SetupFn& setup, + ExecuteFn execute) { + Pass& pass = this->passes.emplace_back(); + pass.name = std::move(name); + pass.execute = std::move(execute); + + PassBuilder builder{*this, pass}; + setup(builder); +} + +void RenderGraph::execute(vk::CommandBuffer commandBuffer) { + for (Pass& pass : this->passes) { + this->recordPass(commandBuffer, pass); + } + + PendingBarriers pending; + for (u32 i = 0; i < this->images.size(); i++) { + this->transitionTo(ImageRef{i}, getAccessState(this->images[i].finalAccess), + pending); + } + this->flush(commandBuffer, pending); + + for (ImportedImage& imported : this->images) { + imported.image->recordLayout(imported.state.layout); + } + + this->passes.clear(); +} + +void RenderGraph::transitionTo(ImageRef ref, const AccessState& next, + PendingBarriers& pending) { + SOPHIA_ASSERT(ref.isValid() && ref.index < this->images.size(), + "ImageRef doesn't belong to this graph"); + + ImportedImage& imported = this->images[ref.index]; + + if (needsBarrier(imported.state, next)) { + pending.barriers.push_back(makeImageBarrier( + imported.image->get(), imported.image->getSubresourceRange(), + imported.state, next)); + pending.srcStages |= imported.state.stage; + pending.dstStages |= next.stage; + imported.state = next; + } else { + // Same-layout read after read. Nothing to record, but a later write has + // to wait on this reader too. + imported.state.stage |= next.stage; + imported.state.access |= next.access; + } +} + +void RenderGraph::flush(vk::CommandBuffer commandBuffer, + PendingBarriers& pending) { + if (pending.barriers.empty()) { + return; + } + + commandBuffer.pipelineBarrier(pending.srcStages, pending.dstStages, {}, + nullptr, nullptr, pending.barriers); + pending = PendingBarriers{}; +} + +void RenderGraph::recordPass(vk::CommandBuffer commandBuffer, Pass& pass) { + beginDebugLabel(commandBuffer, pass.name.c_str()); + + PendingBarriers pending; + for (const ImageUse& use : pass.uses) { + this->transitionTo(use.image, getAccessState(use.access), pending); + } + this->flush(commandBuffer, pending); + + if (pass.colorAttachments.empty()) { + pass.execute(commandBuffer); + } else { + this->recordRendering(commandBuffer, pass); + } + + endDebugLabel(commandBuffer); +} + +void RenderGraph::recordRendering(vk::CommandBuffer commandBuffer, Pass& pass) { + std::vector<vk::RenderingAttachmentInfo> attachments; + attachments.reserve(pass.colorAttachments.size()); + + vk::Extent3D imageExtent = + this->images[pass.colorAttachments.front().image.index] + .image->getExtent(); + vk::Extent2D extent{imageExtent.width, imageExtent.height}; + + for (const ColorAttachment& color : pass.colorAttachments) { + const Image& image = *this->images[color.image.index].image; + SOPHIA_ASSERT(image.getExtent().width == extent.width && + image.getExtent().height == extent.height, + "Pass", pass.name, + "has color attachments of different sizes"); + + vk::RenderingAttachmentInfo attachment{}; + attachment.imageView = image.getView(); + attachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; + attachment.loadOp = color.loadOp; + attachment.storeOp = vk::AttachmentStoreOp::eStore; + attachment.clearValue.color = vk::ClearColorValue{color.clearColor}; + attachments.push_back(attachment); + } + + vk::RenderingInfo renderingInfo{}; + renderingInfo.renderArea = vk::Rect2D{{0, 0}, extent}; + renderingInfo.layerCount = 1; + renderingInfo.colorAttachmentCount = static_cast<u32>(attachments.size()); + renderingInfo.pColorAttachments = attachments.data(); + + commandBuffer.beginRendering(renderingInfo); + + vk::Viewport viewport{0.0f, + 0.0f, + static_cast<float>(extent.width), + static_cast<float>(extent.height), + 0.0f, + 1.0f}; + vk::Rect2D scissor{{0, 0}, extent}; + commandBuffer.setViewport(0, viewport); + commandBuffer.setScissor(0, scissor); + + pass.execute(commandBuffer); + + commandBuffer.endRendering(); +} + +RenderGraph::PassBuilder& RenderGraph::PassBuilder::writeColor( + ImageRef image, vk::AttachmentLoadOp loadOp, ClearColor clearColor) { + this->use(image, Access::ColorWrite); + this->pass.colorAttachments.push_back({image, loadOp, clearColor}); + return *this; +} + +RenderGraph::PassBuilder& RenderGraph::PassBuilder::sample(ImageRef image) { + return this->use(image, Access::SampledRead); +} + +RenderGraph::PassBuilder& RenderGraph::PassBuilder::use(ImageRef image, + Access access) { + SOPHIA_ASSERT(image.isValid() && image.index < this->graph.images.size(), + "ImageRef doesn't belong to this graph"); + for (const ImageUse& existing : this->pass.uses) { + SOPHIA_ASSERT(existing.image.index != image.index, "Pass", this->pass.name, + "uses the same image twice"); + } + + this->pass.uses.push_back({image, access}); + return *this; +} + +} // namespace sophia diff --git a/engine/src/rendering/render_graph.hpp b/engine/src/rendering/render_graph.hpp new file mode 100644 index 0000000..ff2cd32 --- /dev/null +++ b/engine/src/rendering/render_graph.hpp @@ -0,0 +1,149 @@ +#pragma once + +#include <array> +#include <functional> +#include <sophia/types.hpp> +#include <string> +#include <vector> +#include <vulkan/vulkan.hpp> + +#include "vulkan/access.hpp" +#include "vulkan/image.hpp" + +namespace sophia { + +/** + * Names an image within one RenderGraph. Only valid for the graph that + * returned it. + */ +struct ImageRef { + u32 index = INVALID_INDEX; + + static constexpr u32 INVALID_INDEX = 0xFFFFFFFF; + + bool isValid() const { return this->index != INVALID_INDEX; } +}; + +using ClearColor = std::array<float, 4>; + +/** + * A frame's passes and the images they touch, recorded into a single command + * buffer. Passes run in the order they were added; the graph's job is to + * work out the barriers and layout transitions between them from what each + * pass says it does to each image, and to begin/end rendering for passes that + * write color attachments. + * + * Built fresh every frame: + * + * RenderGraph graph; + * ImageRef backbuffer = graph.importImage(swapchainImage, Access::Present); + * graph.addPass( + * "main", [&](PassBuilder& pass) { pass.writeColor(backbuffer); }, + * [&](vk::CommandBuffer cb) { draw(cb); }); + * graph.execute(commandBuffer); + * + * Imported images start in whatever layout Image reports and are left in + * their final access when execute() returns, with the Image's tracked layout + * updated to match. + */ +class RenderGraph { + public: + class PassBuilder; + + using SetupFn = std::function<void(PassBuilder&)>; + using ExecuteFn = std::function<void(vk::CommandBuffer)>; + + RenderGraph() = default; + + RenderGraph(const RenderGraph&) = delete; + RenderGraph& operator=(const RenderGraph&) = delete; + + /** + * Brings an image the graph doesn't own into it. It must outlive + * execute(). After the last pass it is transitioned to `finalAccess`. + */ + ImageRef importImage(Image& image, Access finalAccess); + + /** + * Adds a pass. `setup` runs immediately and declares what the pass does to + * images; `execute` runs later, from execute(), with the barriers already + * recorded and (if the pass writes color) rendering begun. `name` labels + * the pass's commands in debugging tools such as RenderDoc. + */ + void addPass(std::string name, const SetupFn& setup, ExecuteFn execute); + + /** + * Records every pass, and the barriers between them, into `commandBuffer`. + */ + void execute(vk::CommandBuffer commandBuffer); + + private: + struct ImageUse { + ImageRef image; + Access access; + }; + + struct ColorAttachment { + ImageRef image; + vk::AttachmentLoadOp loadOp; + ClearColor clearColor; + }; + + struct Pass { + std::string name; + std::vector<ImageUse> uses; + std::vector<ColorAttachment> colorAttachments; + ExecuteFn execute; + }; + + struct ImportedImage { + Image* image; + Access finalAccess; + // What the image was last left in, as of the barriers recorded so far. + AccessState state; + }; + + struct PendingBarriers { + std::vector<vk::ImageMemoryBarrier> barriers; + vk::PipelineStageFlags srcStages; + vk::PipelineStageFlags dstStages; + }; + + void transitionTo(ImageRef ref, const AccessState& next, + PendingBarriers& pending); + void flush(vk::CommandBuffer commandBuffer, PendingBarriers& pending); + void recordPass(vk::CommandBuffer commandBuffer, Pass& pass); + void recordRendering(vk::CommandBuffer commandBuffer, Pass& pass); + + std::vector<ImportedImage> images; + std::vector<Pass> passes; +}; + +class RenderGraph::PassBuilder { + public: + /** + * Renders into `image` as a color attachment. Passes with one or more of + * these get rendering begun (and viewport/scissor set to the attachment's + * extent) before their execute callback runs. + */ + PassBuilder& writeColor( + ImageRef image, + vk::AttachmentLoadOp loadOp = vk::AttachmentLoadOp::eClear, + ClearColor clearColor = {0.f, 0.f, 0.f, 1.f}); + + /** Samples `image` in a shader. */ + PassBuilder& sample(ImageRef image); + + /** Any other use, e.g. storage or transfer access. */ + PassBuilder& use(ImageRef image, Access access); + + private: + friend class RenderGraph; + + PassBuilder(RenderGraph& graph, Pass& pass) : graph{graph}, pass{pass} {} + + RenderGraph& graph; + Pass& pass; +}; + +} // namespace sophia diff --git a/engine/src/rendering/scene_renderer.cpp b/engine/src/rendering/scene_renderer.cpp new file mode 100644 index 0000000..fc1e8be --- /dev/null +++ b/engine/src/rendering/scene_renderer.cpp @@ -0,0 +1,74 @@ +#include "rendering/scene_renderer.hpp" + +#include <sophia/components.hpp> +#include <sophia/util/assert.hpp> + +namespace sophia { + +namespace { +struct PushConstantData { + glm::mat4 transform{1.f}; + glm::vec4 color{1.f}; +}; +} // namespace + +SceneRenderer::SceneRenderer(const SceneRegistry& sceneRegistry, + ResourceRegistry& resources) + : sceneRegistry{sceneRegistry}, resources{resources} {} + +SceneRenderer::~SceneRenderer() {} + +void SceneRenderer::render(SceneView sceneView) { + auto& handle = sceneView.scene; + auto& scene = this->sceneRegistry.get(handle); + + auto& camera = sceneView.camera; + SOPHIA_ASSERT(camera.hasComponent<CameraComponent>(), + "camera does not have camera component!"); + + auto& registry = scene.registry; + + auto view = registry.view<MeshComponent, TransformComponent>(); + + for (auto entity : view) { + auto& transform = view.get<TransformComponent>(entity); + log::info(transform); + } + + // Get all mesh + // For each mesh, get all entities(ES) that have a transform and that mesh + // For e in ES: add e's transform component to a list + + // We now have + /* + M = mesh + T = transform + + Work Batch + |- M0 + | |- T0 + | \- T2 + | + |- M1 + | |- T1 + | \- T5 + | + \- M2 + |- T3 + \- T4 + + */ + + /* + for batch in Work Batch: + batch.mesh.bind() + + for transform in batch.transforms: + commandBuffer.pushConstants(transform) + batch.mesh.draw(commandBuffer) + + NOTE: Use instanced rendering? + */ +} + +} // namespace sophia diff --git a/engine/src/rendering/scene_renderer.hpp b/engine/src/rendering/scene_renderer.hpp new file mode 100644 index 0000000..abaf69b --- /dev/null +++ b/engine/src/rendering/scene_renderer.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include <sophia/scene_registry.hpp> +#include <sophia/scene_view.hpp> + +#include "resources/resource_registry.hpp" + +namespace sophia { + +/** + * !TEMP to be decoupled once scene batching and rendering impl + */ +class SceneRenderer { + public: + SceneRenderer(const SceneRenderer&) = delete; + SceneRenderer& operator=(const SceneRenderer&) = delete; + + SceneRenderer(const SceneRegistry& sceneRegistry, + ResourceRegistry& resources); + ~SceneRenderer(); + + void render(SceneView sceneView); + + private: + const SceneRegistry& sceneRegistry; + ResourceRegistry& resources; +}; + +} // namespace sophia diff --git a/engine/src/mesh.cpp b/engine/src/resources/mesh.cpp similarity index 99% rename from engine/src/mesh.cpp rename to engine/src/resources/mesh.cpp index b865bad..eba9e28 100644 --- a/engine/src/mesh.cpp +++ b/engine/src/resources/mesh.cpp @@ -1,4 +1,4 @@ -#include "mesh.hpp" +#include "resources/mesh.hpp" #include <sophia/util/assert.hpp> #include <sophia/util/logger.hpp> diff --git a/engine/src/mesh.hpp b/engine/src/resources/mesh.hpp similarity index 87% rename from engine/src/mesh.hpp rename to engine/src/resources/mesh.hpp index 7ee69a8..86bd302 100644 --- a/engine/src/mesh.hpp +++ b/engine/src/resources/mesh.hpp @@ -1,23 +1,18 @@ #pragma once #include <memory> -#include <sophia/asset_registry.hpp> +#include <sophia/handles.hpp> +#include <sophia/math.hpp> #include <sophia/types.hpp> #include <vector> -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include <glm/glm.hpp> - -#include "buffer.hpp" -#include "device.hpp" +#include "vulkan/buffer.hpp" +#include "vulkan/device.hpp" namespace sophia { class Mesh { public: - using HandleType = MeshHandle; - struct Vertex { glm::vec2 position{}; glm::vec3 color{}; diff --git a/engine/src/resources/pools/render_target_pool.cpp b/engine/src/resources/pools/render_target_pool.cpp new file mode 100644 index 0000000..fb3ee70 --- /dev/null +++ b/engine/src/resources/pools/render_target_pool.cpp @@ -0,0 +1,22 @@ +#include "resources/pools/render_target_pool.hpp" + +namespace sophia { + +RenderTargetPool::RenderTargetPool(Device& device, vk::Format format) + : device{device}, format{format} {} + +RenderTargetHandle RenderTargetPool::create(u32 width, u32 height) { + return pool.create(device, format, width, height); +} + +void RenderTargetPool::release(RenderTargetHandle handle) { + SOPHIA_ASSERT(pool.isAlive(handle), "invalid RenderTargetHandle"); + device.waitIdle(); + pool.release(handle); +} + +void RenderTargetPool::applyPendingResizes() { + pool.forEach([](RenderTarget& target) { target.applyPendingResize(); }); +} + +} // namespace sophia diff --git a/engine/src/resources/pools/render_target_pool.hpp b/engine/src/resources/pools/render_target_pool.hpp new file mode 100644 index 0000000..5f4b525 --- /dev/null +++ b/engine/src/resources/pools/render_target_pool.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include <sophia/handles.hpp> +#include <sophia/util/pool.hpp> + +#include "resources/render_target.hpp" +#include "vulkan/device.hpp" + +namespace sophia { + +class RenderTargetPool { + public: + RenderTargetPool(Device& device, vk::Format format); + + RenderTargetPool(const RenderTargetPool&) = delete; + RenderTargetPool& operator=(const RenderTargetPool&) = delete; + + RenderTargetHandle create(u32 width, u32 height); + RenderTarget& get(RenderTargetHandle handle) { return pool.get(handle); } + const RenderTarget& get(RenderTargetHandle handle) const { + return pool.get(handle); + } + void release(RenderTargetHandle handle); + + bool isAlive(RenderTargetHandle handle) const { return pool.isAlive(handle); } + + void applyPendingResizes(); + + private: + Device& device; + vk::Format format; + + Pool<RenderTarget> pool{"RenderTarget"}; +}; + +} // namespace sophia diff --git a/engine/src/resources/render_target.cpp b/engine/src/resources/render_target.cpp new file mode 100644 index 0000000..b4e81e1 --- /dev/null +++ b/engine/src/resources/render_target.cpp @@ -0,0 +1,52 @@ +#include "resources/render_target.hpp" + +#include <algorithm> + +namespace sophia { + +RenderTarget::RenderTarget(Device& device, vk::Format format, u32 width, + u32 height) + : device{device}, format{format} { + create(std::max(width, 1u), std::max(height, 1u)); +} + +RenderTarget::~RenderTarget() { destroy(); } + +vk::Extent2D RenderTarget::getExtent() const { + vk::Extent3D extent = this->image->getExtent(); + return {extent.width, extent.height}; +} + +void RenderTarget::requestResize(u32 width, u32 height) { + this->pendingExtent = vk::Extent2D{std::max(width, 1u), std::max(height, 1u)}; +} + +void RenderTarget::applyPendingResize() { + if (!this->pendingExtent) { + return; + } + + vk::Extent2D extent = *this->pendingExtent; + this->pendingExtent.reset(); + if (extent == getExtent()) { + return; + } + + this->device.waitIdle(); + + destroy(); + create(extent.width, extent.height); +} + +void RenderTarget::create(u32 width, u32 height) { + this->image = Image::Builder(this->device) + .setExtent(width, height) + .setFormat(this->format) + .asColorAttachment() + .withView() + .build(); +} + +void RenderTarget::destroy() { this->image.reset(); } + +} // namespace sophia diff --git a/engine/src/resources/render_target.hpp b/engine/src/resources/render_target.hpp new file mode 100644 index 0000000..fd4f4f8 --- /dev/null +++ b/engine/src/resources/render_target.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include <memory> +#include <optional> +#include <sophia/types.hpp> +#include <vulkan/vulkan.hpp> + +#include "vulkan/device.hpp" +#include "vulkan/image.hpp" + +namespace sophia { + +/** + * A place rendering can go. Currently always backed by an offscreen color + * image. + * + * Owned by the ResourceRegistry and addressed through a RenderTargetHandle. + */ +class RenderTarget { + public: + RenderTarget(Device& device, vk::Format format, u32 width, u32 height); + ~RenderTarget(); + + RenderTarget(const RenderTarget&) = delete; + RenderTarget& operator=(const RenderTarget&) = delete; + + Image& getImage() const { return *this->image; } + + vk::Extent2D getExtent() const; + + void requestResize(u32 width, u32 height); + + /** + * Applies a pending resize. Waits for the GPU, so it must run before any of + * the frame's work (or any UI reference to the image) is recorded. + */ + void applyPendingResize(); + + private: + void create(u32 width, u32 height); + void destroy(); + + Device& device; + vk::Format format; + + std::unique_ptr<Image> image; + + std::optional<vk::Extent2D> pendingExtent; +}; + +} // namespace sophia diff --git a/engine/src/resources/resource_registry.hpp b/engine/src/resources/resource_registry.hpp new file mode 100644 index 0000000..3912cf7 --- /dev/null +++ b/engine/src/resources/resource_registry.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include <sophia/handles.hpp> +#include <sophia/util/pool.hpp> + +#include "resources/mesh.hpp" +#include "resources/pools/render_target_pool.hpp" +#include "vulkan/device.hpp" + +namespace sophia { + +class ResourceRegistry { + public: + ResourceRegistry(const ResourceRegistry&) = delete; + ResourceRegistry& operator=(const ResourceRegistry&) = delete; + + ResourceRegistry(Device& device, vk::Format renderTargetFormat) + : device{device}, renderTargetPool{device, renderTargetFormat} {} + + Device& device; + Pool<Mesh> meshPool{"Mesh"}; + RenderTargetPool renderTargetPool; + // TexturePool textures; + // MaterialPool materials; +}; + +} // namespace sophia diff --git a/engine/src/resources/resource_registry_ref.cpp b/engine/src/resources/resource_registry_ref.cpp new file mode 100644 index 0000000..6d963a8 --- /dev/null +++ b/engine/src/resources/resource_registry_ref.cpp @@ -0,0 +1,59 @@ +#include <sophia/resource_registry_ref.hpp> +#include <sophia/util/assert.hpp> + +#include "resources/resource_registry.hpp" + +namespace sophia { + +template <> +MeshHandle ResourceRegistryRef::load<Mesh>(const std::string& path) { + (void)path; + SOPHIA_ASSERT(false, + "Mesh::load(path) is not implemented yet - no model file " + "format is supported; build one via Mesh::Builder for now"); + return MeshHandle{}; +} + +template <> +void ResourceRegistryRef::release<Mesh>(MeshHandle handle) { + registry->meshPool.release(handle); +} + +template <> +void ResourceRegistryRef::release<RenderTarget>(RenderTargetHandle handle) { + registry->renderTargetPool.release(handle); +} + +MeshHandle ResourceRegistryRef::createResource( + std::type_identity<Mesh>, std::vector<CanvasVertex> vertices, + std::vector<u32> indices) { + Mesh::Builder builder{}; + builder.vertices.reserve(vertices.size()); + for (const CanvasVertex& vertex : vertices) { + builder.vertices.push_back({vertex.position, vertex.color}); + } + builder.indices = std::move(indices); + + return registry->meshPool.create(registry->device, builder); +} + +RenderTargetHandle ResourceRegistryRef::createResource( + std::type_identity<RenderTarget>, u32 width, u32 height) { + return registry->renderTargetPool.create(width, height); +} + +void ResourceRegistryRef::requestResize(RenderTargetHandle handle, u32 width, + u32 height) { + registry->renderTargetPool.get(handle).requestResize(width, height); +} + +glm::vec2 ResourceRegistryRef::getExtent(RenderTargetHandle handle) const { + vk::Extent2D extent = registry->renderTargetPool.get(handle).getExtent(); + return {static_cast<float>(extent.width), static_cast<float>(extent.height)}; +} + +bool ResourceRegistryRef::isAlive(RenderTargetHandle handle) const { + return registry->renderTargetPool.isAlive(handle); +} + +} // namespace sophia diff --git a/engine/src/scene.hpp b/engine/src/scene.hpp deleted file mode 100644 index ed40054..0000000 --- a/engine/src/scene.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include <entt/entt.hpp> - -namespace sophia { - -class Scene { - public: - Scene(); - ~Scene(); - - private: - entt::registry registry; -}; - -} // namespace sophia diff --git a/engine/src/scene/entity.cpp b/engine/src/scene/entity.cpp new file mode 100644 index 0000000..553a759 --- /dev/null +++ b/engine/src/scene/entity.cpp @@ -0,0 +1,8 @@ +#include <sophia/entity.hpp> + +namespace sophia { + +Entity::Entity(entt::entity handle, Scene* scene) + : handle(handle), scene(scene) {} + +} // namespace sophia diff --git a/engine/src/scene.cpp b/engine/src/scene/scene.cpp similarity index 92% rename from engine/src/scene.cpp rename to engine/src/scene/scene.cpp index 865868f..375424b 100644 --- a/engine/src/scene.cpp +++ b/engine/src/scene/scene.cpp @@ -1,6 +1,5 @@ -#include "scene.hpp" - #include <sophia/components.hpp> +#include <sophia/scene.hpp> #include <sophia/util/logger.hpp> namespace sophia { diff --git a/engine/src/shaders/fullscreen.vert.spv.h b/engine/src/shaders/fullscreen.vert.spv.h new file mode 100644 index 0000000..08b2353 --- /dev/null +++ b/engine/src/shaders/fullscreen.vert.spv.h @@ -0,0 +1,100 @@ +#pragma once + +#include <array> +#include <cstdint> + +namespace sophia::shaders { + +inline constexpr std::array<uint32_t, 355> fullscreen_vert_spv = { + 0x07230203, 0x00010000, 0x000d000b, 0x00000032, + 0x00000000, 0x00020011, 0x00000001, 0x0006000b, + 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, + 0x00000000, 0x0003000e, 0x00000000, 0x00000001, + 0x0008000f, 0x00000000, 0x00000004, 0x6e69616d, + 0x00000000, 0x00000018, 0x00000022, 0x0000002c, + 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, + 0x475f4c47, 0x4c474f4f, 0x70635f45, 0x74735f70, + 0x5f656c79, 0x656e696c, 0x7269645f, 0x69746365, + 0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f, + 0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, + 0x00657669, 0x00040005, 0x00000004, 0x6e69616d, + 0x00000000, 0x00050005, 0x0000000c, 0x69736f70, + 0x6e6f6974, 0x00000073, 0x00030005, 0x00000015, + 0x00736f70, 0x00060005, 0x00000018, 0x565f6c67, + 0x65747265, 0x646e4978, 0x00007865, 0x00060005, + 0x00000020, 0x505f6c67, 0x65567265, 0x78657472, + 0x00000000, 0x00060006, 0x00000020, 0x00000000, + 0x505f6c67, 0x7469736f, 0x006e6f69, 0x00070006, + 0x00000020, 0x00000001, 0x505f6c67, 0x746e696f, + 0x657a6953, 0x00000000, 0x00070006, 0x00000020, + 0x00000002, 0x435f6c67, 0x4470696c, 0x61747369, + 0x0065636e, 0x00070006, 0x00000020, 0x00000003, + 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e, + 0x00030005, 0x00000022, 0x00000000, 0x00030005, + 0x0000002c, 0x00007675, 0x00040047, 0x00000018, + 0x0000000b, 0x0000002a, 0x00030047, 0x00000020, + 0x00000002, 0x00050048, 0x00000020, 0x00000000, + 0x0000000b, 0x00000000, 0x00050048, 0x00000020, + 0x00000001, 0x0000000b, 0x00000001, 0x00050048, + 0x00000020, 0x00000002, 0x0000000b, 0x00000003, + 0x00050048, 0x00000020, 0x00000003, 0x0000000b, + 0x00000004, 0x00040047, 0x0000002c, 0x0000001e, + 0x00000000, 0x00020013, 0x00000002, 0x00030021, + 0x00000003, 0x00000002, 0x00030016, 0x00000006, + 0x00000020, 0x00040017, 0x00000007, 0x00000006, + 0x00000002, 0x00040015, 0x00000008, 0x00000020, + 0x00000000, 0x0004002b, 0x00000008, 0x00000009, + 0x00000006, 0x0004001c, 0x0000000a, 0x00000007, + 0x00000009, 0x00040020, 0x0000000b, 0x00000006, + 0x0000000a, 0x0004003b, 0x0000000b, 0x0000000c, + 0x00000006, 0x0004002b, 0x00000006, 0x0000000d, + 0xbf800000, 0x0005002c, 0x00000007, 0x0000000e, + 0x0000000d, 0x0000000d, 0x0004002b, 0x00000006, + 0x0000000f, 0x3f800000, 0x0005002c, 0x00000007, + 0x00000010, 0x0000000f, 0x0000000d, 0x0005002c, + 0x00000007, 0x00000011, 0x0000000d, 0x0000000f, + 0x0005002c, 0x00000007, 0x00000012, 0x0000000f, + 0x0000000f, 0x0009002c, 0x0000000a, 0x00000013, + 0x0000000e, 0x00000010, 0x00000011, 0x00000011, + 0x00000010, 0x00000012, 0x00040020, 0x00000014, + 0x00000007, 0x00000007, 0x00040015, 0x00000016, + 0x00000020, 0x00000001, 0x00040020, 0x00000017, + 0x00000001, 0x00000016, 0x0004003b, 0x00000017, + 0x00000018, 0x00000001, 0x00040020, 0x0000001a, + 0x00000006, 0x00000007, 0x00040017, 0x0000001d, + 0x00000006, 0x00000004, 0x0004002b, 0x00000008, + 0x0000001e, 0x00000001, 0x0004001c, 0x0000001f, + 0x00000006, 0x0000001e, 0x0006001e, 0x00000020, + 0x0000001d, 0x00000006, 0x0000001f, 0x0000001f, + 0x00040020, 0x00000021, 0x00000003, 0x00000020, + 0x0004003b, 0x00000021, 0x00000022, 0x00000003, + 0x0004002b, 0x00000016, 0x00000023, 0x00000000, + 0x0004002b, 0x00000006, 0x00000025, 0x00000000, + 0x00040020, 0x00000029, 0x00000003, 0x0000001d, + 0x00040020, 0x0000002b, 0x00000003, 0x00000007, + 0x0004003b, 0x0000002b, 0x0000002c, 0x00000003, + 0x0004002b, 0x00000006, 0x0000002e, 0x3f000000, + 0x00050036, 0x00000002, 0x00000004, 0x00000000, + 0x00000003, 0x000200f8, 0x00000005, 0x0004003b, + 0x00000014, 0x00000015, 0x00000007, 0x0003003e, + 0x0000000c, 0x00000013, 0x0004003d, 0x00000016, + 0x00000019, 0x00000018, 0x00050041, 0x0000001a, + 0x0000001b, 0x0000000c, 0x00000019, 0x0004003d, + 0x00000007, 0x0000001c, 0x0000001b, 0x0003003e, + 0x00000015, 0x0000001c, 0x0004003d, 0x00000007, + 0x00000024, 0x00000015, 0x00050051, 0x00000006, + 0x00000026, 0x00000024, 0x00000000, 0x00050051, + 0x00000006, 0x00000027, 0x00000024, 0x00000001, + 0x00070050, 0x0000001d, 0x00000028, 0x00000026, + 0x00000027, 0x00000025, 0x0000000f, 0x00050041, + 0x00000029, 0x0000002a, 0x00000022, 0x00000023, + 0x0003003e, 0x0000002a, 0x00000028, 0x0004003d, + 0x00000007, 0x0000002d, 0x00000015, 0x0005008e, + 0x00000007, 0x0000002f, 0x0000002d, 0x0000002e, + 0x00050050, 0x00000007, 0x00000030, 0x0000002e, + 0x0000002e, 0x00050081, 0x00000007, 0x00000031, + 0x0000002f, 0x00000030, 0x0003003e, 0x0000002c, + 0x00000031, 0x000100fd, 0x00010038, +}; + +} // namespace sophia::shaders diff --git a/engine/src/shaders/model_render_system.frag.spv.h b/engine/src/shaders/model_render_system.frag.spv.h new file mode 100644 index 0000000..e5f1c2f --- /dev/null +++ b/engine/src/shaders/model_render_system.frag.spv.h @@ -0,0 +1,35 @@ +#pragma once + +#include <array> +#include <cstdint> + +namespace sophia::shaders { + +inline constexpr std::array<uint32_t, 143> model_render_system_frag_spv = { + 0x07230203, 0x00010000, 0x000d000b, 0x00000013, 0x00000000, 0x00020011, + 0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, + 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0007000f, 0x00000004, + 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x0000000c, 0x00030010, + 0x00000004, 0x00000007, 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, + 0x475f4c47, 0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c, + 0x7269645f, 0x69746365, 0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f, + 0x6e695f45, 0x64756c63, 0x69645f65, 0x74636572, 0x00657669, 0x00040005, + 0x00000004, 0x6e69616d, 0x00000000, 0x00050005, 0x00000009, 0x4374756f, + 0x726f6c6f, 0x00000000, 0x00050005, 0x0000000c, 0x67617266, 0x6f6c6f43, + 0x00000072, 0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00040047, + 0x0000000c, 0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021, + 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, + 0x00000007, 0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003, + 0x00000007, 0x0004003b, 0x00000008, 0x00000009, 0x00000003, 0x00040017, + 0x0000000a, 0x00000006, 0x00000003, 0x00040020, 0x0000000b, 0x00000001, + 0x0000000a, 0x0004003b, 0x0000000b, 0x0000000c, 0x00000001, 0x0004002b, + 0x00000006, 0x0000000e, 0x3f800000, 0x00050036, 0x00000002, 0x00000004, + 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003d, 0x0000000a, + 0x0000000d, 0x0000000c, 0x00050051, 0x00000006, 0x0000000f, 0x0000000d, + 0x00000000, 0x00050051, 0x00000006, 0x00000010, 0x0000000d, 0x00000001, + 0x00050051, 0x00000006, 0x00000011, 0x0000000d, 0x00000002, 0x00070050, + 0x00000007, 0x00000012, 0x0000000f, 0x00000010, 0x00000011, 0x0000000e, + 0x0003003e, 0x00000009, 0x00000012, 0x000100fd, 0x00010038, +}; + +} // namespace sophia::shaders diff --git a/engine/src/shaders/model_render_system.vert.spv.h b/engine/src/shaders/model_render_system.vert.spv.h new file mode 100644 index 0000000..0ad2b04 --- /dev/null +++ b/engine/src/shaders/model_render_system.vert.spv.h @@ -0,0 +1,69 @@ +#pragma once + +#include <array> +#include <cstdint> + +namespace sophia::shaders { + +inline constexpr std::array<uint32_t, 345> model_render_system_vert_spv = { + 0x07230203, 0x00010000, 0x000d000b, 0x00000029, 0x00000000, 0x00020011, + 0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, + 0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0009000f, 0x00000000, + 0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00000019, 0x00000025, + 0x00000027, 0x00030003, 0x00000002, 0x000001c2, 0x000a0004, 0x475f4c47, + 0x4c474f4f, 0x70635f45, 0x74735f70, 0x5f656c79, 0x656e696c, 0x7269645f, + 0x69746365, 0x00006576, 0x00080004, 0x475f4c47, 0x4c474f4f, 0x6e695f45, + 0x64756c63, 0x69645f65, 0x74636572, 0x00657669, 0x00040005, 0x00000004, + 0x6e69616d, 0x00000000, 0x00060005, 0x0000000b, 0x505f6c67, 0x65567265, + 0x78657472, 0x00000000, 0x00060006, 0x0000000b, 0x00000000, 0x505f6c67, + 0x7469736f, 0x006e6f69, 0x00070006, 0x0000000b, 0x00000001, 0x505f6c67, + 0x746e696f, 0x657a6953, 0x00000000, 0x00070006, 0x0000000b, 0x00000002, + 0x435f6c67, 0x4470696c, 0x61747369, 0x0065636e, 0x00070006, 0x0000000b, + 0x00000003, 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e, 0x00030005, + 0x0000000d, 0x00000000, 0x00040005, 0x00000011, 0x68737550, 0x00000000, + 0x00060006, 0x00000011, 0x00000000, 0x6e617274, 0x726f6673, 0x0000006d, + 0x00050006, 0x00000011, 0x00000001, 0x6f6c6f63, 0x00000072, 0x00040005, + 0x00000013, 0x68737570, 0x00000000, 0x00050005, 0x00000019, 0x69736f70, + 0x6e6f6974, 0x00000000, 0x00050005, 0x00000025, 0x67617266, 0x6f6c6f43, + 0x00000072, 0x00040005, 0x00000027, 0x6f6c6f63, 0x00000072, 0x00030047, + 0x0000000b, 0x00000002, 0x00050048, 0x0000000b, 0x00000000, 0x0000000b, + 0x00000000, 0x00050048, 0x0000000b, 0x00000001, 0x0000000b, 0x00000001, + 0x00050048, 0x0000000b, 0x00000002, 0x0000000b, 0x00000003, 0x00050048, + 0x0000000b, 0x00000003, 0x0000000b, 0x00000004, 0x00030047, 0x00000011, + 0x00000002, 0x00040048, 0x00000011, 0x00000000, 0x00000005, 0x00050048, + 0x00000011, 0x00000000, 0x00000007, 0x00000010, 0x00050048, 0x00000011, + 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x00000011, 0x00000001, + 0x00000023, 0x00000040, 0x00040047, 0x00000019, 0x0000001e, 0x00000000, + 0x00040047, 0x00000025, 0x0000001e, 0x00000000, 0x00040047, 0x00000027, + 0x0000001e, 0x00000001, 0x00020013, 0x00000002, 0x00030021, 0x00000003, + 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, + 0x00000006, 0x00000004, 0x00040015, 0x00000008, 0x00000020, 0x00000000, + 0x0004002b, 0x00000008, 0x00000009, 0x00000001, 0x0004001c, 0x0000000a, + 0x00000006, 0x00000009, 0x0006001e, 0x0000000b, 0x00000007, 0x00000006, + 0x0000000a, 0x0000000a, 0x00040020, 0x0000000c, 0x00000003, 0x0000000b, + 0x0004003b, 0x0000000c, 0x0000000d, 0x00000003, 0x00040015, 0x0000000e, + 0x00000020, 0x00000001, 0x0004002b, 0x0000000e, 0x0000000f, 0x00000000, + 0x00040018, 0x00000010, 0x00000007, 0x00000004, 0x0004001e, 0x00000011, + 0x00000010, 0x00000007, 0x00040020, 0x00000012, 0x00000009, 0x00000011, + 0x0004003b, 0x00000012, 0x00000013, 0x00000009, 0x00040020, 0x00000014, + 0x00000009, 0x00000010, 0x00040017, 0x00000017, 0x00000006, 0x00000002, + 0x00040020, 0x00000018, 0x00000001, 0x00000017, 0x0004003b, 0x00000018, + 0x00000019, 0x00000001, 0x0004002b, 0x00000006, 0x0000001b, 0x00000000, + 0x0004002b, 0x00000006, 0x0000001c, 0x3f800000, 0x00040020, 0x00000021, + 0x00000003, 0x00000007, 0x00040017, 0x00000023, 0x00000006, 0x00000003, + 0x00040020, 0x00000024, 0x00000003, 0x00000023, 0x0004003b, 0x00000024, + 0x00000025, 0x00000003, 0x00040020, 0x00000026, 0x00000001, 0x00000023, + 0x0004003b, 0x00000026, 0x00000027, 0x00000001, 0x00050036, 0x00000002, + 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x00050041, + 0x00000014, 0x00000015, 0x00000013, 0x0000000f, 0x0004003d, 0x00000010, + 0x00000016, 0x00000015, 0x0004003d, 0x00000017, 0x0000001a, 0x00000019, + 0x00050051, 0x00000006, 0x0000001d, 0x0000001a, 0x00000000, 0x00050051, + 0x00000006, 0x0000001e, 0x0000001a, 0x00000001, 0x00070050, 0x00000007, + 0x0000001f, 0x0000001d, 0x0000001e, 0x0000001b, 0x0000001c, 0x00050091, + 0x00000007, 0x00000020, 0x00000016, 0x0000001f, 0x00050041, 0x00000021, + 0x00000022, 0x0000000d, 0x0000000f, 0x0003003e, 0x00000022, 0x00000020, + 0x0004003d, 0x00000023, 0x00000028, 0x00000027, 0x0003003e, 0x00000025, + 0x00000028, 0x000100fd, 0x00010038, +}; + +} // namespace sophia::shaders diff --git a/engine/src/ui/debug_ui.cpp b/engine/src/ui/debug_ui.cpp index f52cf17..ffd5d17 100644 --- a/engine/src/ui/debug_ui.cpp +++ b/engine/src/ui/debug_ui.cpp @@ -12,6 +12,7 @@ DebugUI::DebugUI(PanelRegistry& registry, ConsoleCommandRegistry& commands) this->panelRegistryHandle = registry.emplace<PanelRegistryPanel>(registry); this->frameStatsHandle = registry.emplace<FrameStatsPanel>(); this->consoleHandle = registry.emplace<ConsolePanel>(commands); + hide(); // #endif // !NDEBUG } diff --git a/engine/src/ui/panel_registry.cpp b/engine/src/ui/panel_registry.cpp index 5b34ea9..f372ffd 100644 --- a/engine/src/ui/panel_registry.cpp +++ b/engine/src/ui/panel_registry.cpp @@ -1,88 +1,23 @@ #include <sophia/ui/panel_registry.hpp> -#include <sophia/util/assert.hpp> namespace sophia { -namespace { -constexpr u32 GENERATION_MASK = (1u << 12) - 1; -} // namespace - -PanelHandle PanelRegistry::add(std::unique_ptr<Panel> panel) { - if (!freeList.empty()) { - u32 index = freeList.back(); - freeList.pop_back(); - slots[index].panel = std::move(panel); - return PanelHandle{index, slots[index].generation}; - } - - slots.push_back(Slot{std::move(panel), 0}); - return PanelHandle{static_cast<u32>(slots.size() - 1), 0}; -} - -const Panel& PanelRegistry::get(PanelHandle handle) const { - SOPHIA_ASSERT(handle.isValid(), "invalid PanelHandle"); - SOPHIA_ASSERT(handle.index < slots.size(), - "PanelHandle index out of range:", handle.index); - - const Slot& slot = slots[handle.index]; - SOPHIA_ASSERT(slot.generation == handle.generation, - "stale PanelHandle (generation mismatch):", handle.index); - SOPHIA_ASSERT(slot.panel != nullptr, - "PanelHandle refers to a removed panel:", handle.index); - return *slot.panel; -} - -void PanelRegistry::remove(PanelHandle handle) { - SOPHIA_ASSERT(handle.isValid(), "invalid PanelHandle"); - SOPHIA_ASSERT(handle.index < slots.size(), - "PanelHandle index out of range:", handle.index); - - Slot& slot = slots[handle.index]; - SOPHIA_ASSERT(slot.generation == handle.generation, - "stale PanelHandle (generation mismatch):", handle.index); - SOPHIA_ASSERT(slot.panel != nullptr, - "PanelHandle already removed:", handle.index); - - slot.panel.reset(); - slot.generation = (slot.generation + 1) & GENERATION_MASK; - freeList.push_back(handle.index); -} - -bool PanelRegistry::isValid(PanelHandle handle) const { - if (!handle.isValid() || handle.index >= slots.size()) { - return false; - } - - const Slot& slot = slots[handle.index]; - return slot.generation == handle.generation && slot.panel != nullptr; -} - -u32 PanelRegistry::size() const { - u32 count = 0; - for (const Slot& slot : slots) { - if (slot.panel) { - count++; - } - } - return count; -} - u32 PanelRegistry::visibleCount() const { u32 count = 0; - for (const Slot& slot : slots) { - if (slot.panel && slot.panel->isVisible()) { + pool.forEach([&count](const Panel& panel) { + if (panel.isVisible()) { count++; } - } + }); return count; } void PanelRegistry::update(const FrameInfo& frameInfo) { - for (auto& slot : slots) { - if (slot.panel && slot.panel->isVisible()) { - slot.panel->draw(frameInfo); + pool.forEach([&frameInfo](Panel& panel) { + if (panel.isVisible()) { + panel.draw(frameInfo); } - } + }); } } // namespace sophia diff --git a/engine/src/ui/ui.cpp b/engine/src/ui/ui.cpp new file mode 100644 index 0000000..164722a --- /dev/null +++ b/engine/src/ui/ui.cpp @@ -0,0 +1,26 @@ +#include <sophia/ui/ui.hpp> +#include <sophia/util/assert.hpp> + +#include "rendering/imgui_renderer.hpp" + +namespace sophia { + +void UI::image(RenderTargetHandle handle) { + const ImVec2 size = ImGui::GetContentRegionAvail(); + if (size.x < 1.0f || size.y < 1.0f) { + return; + } + + resources.requestResize(handle, static_cast<u32>(size.x), + static_cast<u32>(size.y)); + ImTextureID textureId = getTextureId(handle); + ImGui::Image(textureId, size); +} + +ImTextureID UI::getTextureId(RenderTargetHandle handle) { + SOPHIA_ASSERT(renderer != nullptr, + "UI::getTextureId() called before the UI was set up"); + return renderer->getTextureId(handle); +} + +} // namespace sophia diff --git a/engine/src/vulkan/access.cpp b/engine/src/vulkan/access.cpp new file mode 100644 index 0000000..640e369 --- /dev/null +++ b/engine/src/vulkan/access.cpp @@ -0,0 +1,104 @@ +#include "vulkan/access.hpp" + +#include <array> +#include <sophia/util/logger.hpp> +#include <stdexcept> + +namespace sophia { + +namespace { + +constexpr std::array<Access, 7> ALL_ACCESSES = { + Access::ColorWrite, Access::DepthWrite, Access::SampledRead, + Access::StorageReadWrite, Access::TransferSrc, Access::TransferDst, + Access::Present, +}; + +constexpr vk::AccessFlags WRITE_ACCESSES = + vk::AccessFlagBits::eColorAttachmentWrite | + vk::AccessFlagBits::eDepthStencilAttachmentWrite | + vk::AccessFlagBits::eShaderWrite | vk::AccessFlagBits::eTransferWrite; + +} // namespace + +AccessState getAccessState(Access access) { + switch (access) { + case Access::ColorWrite: + return {vk::ImageLayout::eColorAttachmentOptimal, + vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::AccessFlagBits::eColorAttachmentRead | + vk::AccessFlagBits::eColorAttachmentWrite}; + case Access::DepthWrite: + return {vk::ImageLayout::eDepthStencilAttachmentOptimal, + vk::PipelineStageFlagBits::eEarlyFragmentTests | + vk::PipelineStageFlagBits::eLateFragmentTests, + vk::AccessFlagBits::eDepthStencilAttachmentRead | + vk::AccessFlagBits::eDepthStencilAttachmentWrite}; + case Access::SampledRead: + return {vk::ImageLayout::eShaderReadOnlyOptimal, + vk::PipelineStageFlagBits::eFragmentShader, + vk::AccessFlagBits::eShaderRead}; + case Access::StorageReadWrite: + return { + vk::ImageLayout::eGeneral, vk::PipelineStageFlagBits::eComputeShader, + vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite}; + case Access::TransferSrc: + return {vk::ImageLayout::eTransferSrcOptimal, + vk::PipelineStageFlagBits::eTransfer, + vk::AccessFlagBits::eTransferRead}; + case Access::TransferDst: + return {vk::ImageLayout::eTransferDstOptimal, + vk::PipelineStageFlagBits::eTransfer, + vk::AccessFlagBits::eTransferWrite}; + case Access::Present: + return {vk::ImageLayout::ePresentSrcKHR, + vk::PipelineStageFlagBits::eBottomOfPipe, vk::AccessFlags{}}; + } + + log::fatal("Unknown Access value."); + throw std::runtime_error("Unknown Access value."); +} + +AccessState getStateForLayout(vk::ImageLayout layout) { + if (layout == vk::ImageLayout::eUndefined) { + return AccessState{}; + } + + for (Access access : ALL_ACCESSES) { + AccessState state = getAccessState(access); + if (state.layout == layout) { + return state; + } + } + + log::fatal("Unsupported image layout:", vk::to_string(layout)); + throw std::runtime_error("Unsupported image layout."); +} + +bool writesImage(const AccessState& state) { + return static_cast<bool>(state.access & WRITE_ACCESSES); +} + +bool needsBarrier(const AccessState& from, const AccessState& to) { + return from.layout != to.layout || writesImage(from) || writesImage(to); +} + +vk::ImageMemoryBarrier makeImageBarrier(vk::Image image, + const vk::ImageSubresourceRange& range, + const AccessState& from, + const AccessState& to) { + vk::ImageMemoryBarrier barrier{}; + barrier.oldLayout = from.layout; + barrier.newLayout = to.layout; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange = range; + // Only writes need to be made available; reads just need the execution + // dependency the stage masks already give. + barrier.srcAccessMask = from.access & WRITE_ACCESSES; + barrier.dstAccessMask = to.access; + return barrier; +} + +} // namespace sophia diff --git a/engine/src/vulkan/access.hpp b/engine/src/vulkan/access.hpp new file mode 100644 index 0000000..67c35f4 --- /dev/null +++ b/engine/src/vulkan/access.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include <vulkan/vulkan.hpp> + +namespace sophia { + +/** + * How an image is used at a point in a frame. Each value implies an image + * layout plus the pipeline stages and memory accesses that go with it, so a + * barrier between any two of them can be derived instead of hand-written. + */ +enum class Access { + ColorWrite, + DepthWrite, + SampledRead, + StorageReadWrite, + TransferSrc, + TransferDst, + Present, +}; + +/** + * The layout, stages and accesses an image is in after some use. Barriers go + * from one of these to another. + */ +struct AccessState { + vk::ImageLayout layout = vk::ImageLayout::eUndefined; + vk::PipelineStageFlags stage = vk::PipelineStageFlagBits::eTopOfPipe; + vk::AccessFlags access; +}; + +/** + * The stage RenderContext waits on for the swapchain image-available + * semaphore. A barrier that takes a swapchain image out of its + * just-acquired state has to use this as its source stage, otherwise the + * layout transition isn't ordered after the acquire. + */ +inline constexpr vk::PipelineStageFlags SWAPCHAIN_ACQUIRE_STAGE = + vk::PipelineStageFlagBits::eColorAttachmentOutput; + +AccessState getAccessState(Access access); + +/** + * The state of an image sitting in `layout` with no outstanding work. + * eUndefined maps to "nothing to wait on"; layouts with no matching Access + * throw. + */ +AccessState getStateForLayout(vk::ImageLayout layout); + +bool writesImage(const AccessState& state); + +/** + * Whether going from `from` to `to` needs a barrier: a layout change, or any + * hazard involving a write. Read-after-read in the same layout doesn't. + */ +bool needsBarrier(const AccessState& from, const AccessState& to); + +vk::ImageMemoryBarrier makeImageBarrier(vk::Image image, + const vk::ImageSubresourceRange& range, + const AccessState& from, + const AccessState& to); + +} // namespace sophia diff --git a/engine/src/buffer.cpp b/engine/src/vulkan/buffer.cpp similarity index 99% rename from engine/src/buffer.cpp rename to engine/src/vulkan/buffer.cpp index 341c436..88428af 100644 --- a/engine/src/buffer.cpp +++ b/engine/src/vulkan/buffer.cpp @@ -2,7 +2,7 @@ * Based on * https://github.com/blurrypiano/littleVulkanEngine/blob/main/src/lve_buffer.cpp */ -#include "buffer.hpp" +#include "vulkan/buffer.hpp" #include <sophia/util/assert.hpp> diff --git a/engine/src/buffer.hpp b/engine/src/vulkan/buffer.hpp similarity index 98% rename from engine/src/buffer.hpp rename to engine/src/vulkan/buffer.hpp index 7c313fa..2320ab4 100644 --- a/engine/src/buffer.hpp +++ b/engine/src/vulkan/buffer.hpp @@ -4,7 +4,7 @@ */ #pragma once -#include "device.hpp" +#include "vulkan/device.hpp" namespace sophia { diff --git a/engine/src/vulkan/debug_label.cpp b/engine/src/vulkan/debug_label.cpp new file mode 100644 index 0000000..3d44e45 --- /dev/null +++ b/engine/src/vulkan/debug_label.cpp @@ -0,0 +1,34 @@ +#include "vulkan/debug_label.hpp" + +namespace sophia { + +static PFN_vkCmdBeginDebugUtilsLabelEXT cmdBeginDebugUtilsLabel = nullptr; +static PFN_vkCmdEndDebugUtilsLabelEXT cmdEndDebugUtilsLabel = nullptr; + +void loadDebugLabelFunctions(vk::Instance instance) { + cmdBeginDebugUtilsLabel = reinterpret_cast<PFN_vkCmdBeginDebugUtilsLabelEXT>( + vkGetInstanceProcAddr(instance, "vkCmdBeginDebugUtilsLabelEXT")); + cmdEndDebugUtilsLabel = reinterpret_cast<PFN_vkCmdEndDebugUtilsLabelEXT>( + vkGetInstanceProcAddr(instance, "vkCmdEndDebugUtilsLabelEXT")); +} + +void beginDebugLabel(vk::CommandBuffer commandBuffer, const char* name) { + if (!cmdBeginDebugUtilsLabel) { + return; + } + + VkDebugUtilsLabelEXT label{}; + label.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT; + label.pLabelName = name; + cmdBeginDebugUtilsLabel(commandBuffer, &label); +} + +void endDebugLabel(vk::CommandBuffer commandBuffer) { + if (!cmdEndDebugUtilsLabel) { + return; + } + + cmdEndDebugUtilsLabel(commandBuffer); +} + +} // namespace sophia diff --git a/engine/src/vulkan/debug_label.hpp b/engine/src/vulkan/debug_label.hpp new file mode 100644 index 0000000..95401c4 --- /dev/null +++ b/engine/src/vulkan/debug_label.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include <vulkan/vulkan.hpp> + +namespace sophia { + +void loadDebugLabelFunctions(vk::Instance instance); + +void beginDebugLabel(vk::CommandBuffer commandBuffer, const char* name); + +void endDebugLabel(vk::CommandBuffer commandBuffer); + +} // namespace sophia diff --git a/engine/src/descriptors/descriptor_pool.cpp b/engine/src/vulkan/descriptors/descriptor_pool.cpp similarity index 100% rename from engine/src/descriptors/descriptor_pool.cpp rename to engine/src/vulkan/descriptors/descriptor_pool.cpp diff --git a/engine/src/descriptors/descriptor_pool.hpp b/engine/src/vulkan/descriptors/descriptor_pool.hpp similarity index 97% rename from engine/src/descriptors/descriptor_pool.hpp rename to engine/src/vulkan/descriptors/descriptor_pool.hpp index 78ee0c7..dcab1d2 100644 --- a/engine/src/descriptors/descriptor_pool.hpp +++ b/engine/src/vulkan/descriptors/descriptor_pool.hpp @@ -3,7 +3,7 @@ #include <memory> #include <vector> -#include "device.hpp" +#include "vulkan/device.hpp" namespace sophia { diff --git a/engine/src/descriptors/descriptor_set_layout.cpp b/engine/src/vulkan/descriptors/descriptor_set_layout.cpp similarity index 100% rename from engine/src/descriptors/descriptor_set_layout.cpp rename to engine/src/vulkan/descriptors/descriptor_set_layout.cpp diff --git a/engine/src/descriptors/descriptor_set_layout.hpp b/engine/src/vulkan/descriptors/descriptor_set_layout.hpp similarity index 97% rename from engine/src/descriptors/descriptor_set_layout.hpp rename to engine/src/vulkan/descriptors/descriptor_set_layout.hpp index ff75b55..b56d0da 100644 --- a/engine/src/descriptors/descriptor_set_layout.hpp +++ b/engine/src/vulkan/descriptors/descriptor_set_layout.hpp @@ -4,7 +4,7 @@ #include <unordered_map> #include <vector> -#include "device.hpp" +#include "vulkan/device.hpp" namespace sophia { diff --git a/engine/src/descriptors/descriptor_writer.cpp b/engine/src/vulkan/descriptors/descriptor_writer.cpp similarity index 100% rename from engine/src/descriptors/descriptor_writer.cpp rename to engine/src/vulkan/descriptors/descriptor_writer.cpp diff --git a/engine/src/descriptors/descriptor_writer.hpp b/engine/src/vulkan/descriptors/descriptor_writer.hpp similarity index 95% rename from engine/src/descriptors/descriptor_writer.hpp rename to engine/src/vulkan/descriptors/descriptor_writer.hpp index 95effbb..47e53c8 100644 --- a/engine/src/descriptors/descriptor_writer.hpp +++ b/engine/src/vulkan/descriptors/descriptor_writer.hpp @@ -4,7 +4,7 @@ #include "descriptor_pool.hpp" #include "descriptor_set_layout.hpp" -#include "device.hpp" +#include "vulkan/device.hpp" namespace sophia { diff --git a/engine/src/device.cpp b/engine/src/vulkan/device.cpp similarity index 99% rename from engine/src/device.cpp rename to engine/src/vulkan/device.cpp index e909b26..2e8fc9c 100644 --- a/engine/src/device.cpp +++ b/engine/src/vulkan/device.cpp @@ -1,10 +1,12 @@ -#include "device.hpp" +#include "vulkan/device.hpp" #include <string.h> #include <set> #include <sophia/util/logger.hpp> +#include "vulkan/debug_label.hpp" + namespace sophia { static VKAPI_ATTR vk::Bool32 VKAPI_CALL @@ -288,6 +290,8 @@ void Device::setupDebugMessenger() { nullptr, &this->debugMessenger) != VK_SUCCESS) { throw std::runtime_error("failed to set up debug callback!"); } + + loadDebugLabelFunctions(*instance); } bool Device::checkValidationLayerSupport() { diff --git a/engine/src/device.hpp b/engine/src/vulkan/device.hpp similarity index 100% rename from engine/src/device.hpp rename to engine/src/vulkan/device.hpp diff --git a/engine/src/image.cpp b/engine/src/vulkan/image.cpp similarity index 72% rename from engine/src/image.cpp rename to engine/src/vulkan/image.cpp index 00ff258..5ea66c1 100644 --- a/engine/src/image.cpp +++ b/engine/src/vulkan/image.cpp @@ -1,81 +1,42 @@ -#include "image.hpp" +#include "vulkan/image.hpp" #include <cstring> #include <sophia/util/logger.hpp> #include <stdexcept> -#include "device.hpp" +#include "vulkan/device.hpp" namespace sophia { -void Image::transitionLayout(vk::CommandBuffer commandBuffer, - vk::ImageLayout newLayout) { - vk::ImageMemoryBarrier barrier{}; - barrier.oldLayout = this->layout; - barrier.newLayout = newLayout; - barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - barrier.image = this->get(); - barrier.subresourceRange.aspectMask = this->aspectMask; - barrier.subresourceRange.baseMipLevel = 0; - barrier.subresourceRange.levelCount = this->mipLevels; - barrier.subresourceRange.baseArrayLayer = 0; - barrier.subresourceRange.layerCount = this->arrayLayers; +vk::ImageSubresourceRange Image::getSubresourceRange() const { + return vk::ImageSubresourceRange{this->aspectMask, 0, this->mipLevels, 0, + this->arrayLayers}; +} - vk::PipelineStageFlags srcStage; - vk::PipelineStageFlags dstStage; +AccessState Image::getAccessState() const { + AccessState state = getStateForLayout(this->layout); - if (this->layout == vk::ImageLayout::eUndefined && - newLayout == vk::ImageLayout::eTransferDstOptimal) { - barrier.srcAccessMask = {}; - barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite; - srcStage = vk::PipelineStageFlagBits::eTopOfPipe; - dstStage = vk::PipelineStageFlagBits::eTransfer; - } else if (this->layout == vk::ImageLayout::eTransferDstOptimal && - newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) { - barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; - barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; - srcStage = vk::PipelineStageFlagBits::eTransfer; - dstStage = vk::PipelineStageFlagBits::eFragmentShader; - } else if (this->layout == vk::ImageLayout::eUndefined && - newLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) { - barrier.srcAccessMask = {}; - barrier.dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead | - vk::AccessFlagBits::eDepthStencilAttachmentWrite; - srcStage = vk::PipelineStageFlagBits::eTopOfPipe; - dstStage = vk::PipelineStageFlagBits::eEarlyFragmentTests; - } else if (this->layout == vk::ImageLayout::eUndefined && - newLayout == vk::ImageLayout::eColorAttachmentOptimal) { - barrier.srcAccessMask = {}; - barrier.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; - srcStage = vk::PipelineStageFlagBits::eTopOfPipe; - dstStage = vk::PipelineStageFlagBits::eColorAttachmentOutput; - } else if (this->layout == vk::ImageLayout::eUndefined && - newLayout == vk::ImageLayout::eGeneral) { - barrier.srcAccessMask = {}; - barrier.dstAccessMask = - vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite; - srcStage = vk::PipelineStageFlagBits::eTopOfPipe; - dstStage = vk::PipelineStageFlagBits::eComputeShader; - } else if (this->layout == vk::ImageLayout::eColorAttachmentOptimal && - newLayout == vk::ImageLayout::ePresentSrcKHR) { - barrier.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; - barrier.dstAccessMask = {}; - srcStage = vk::PipelineStageFlagBits::eColorAttachmentOutput; - dstStage = vk::PipelineStageFlagBits::eBottomOfPipe; - } else if (this->layout == vk::ImageLayout::ePresentSrcKHR && - newLayout == vk::ImageLayout::eColorAttachmentOptimal) { - barrier.srcAccessMask = {}; - barrier.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; - srcStage = vk::PipelineStageFlagBits::eTopOfPipe; - dstStage = vk::PipelineStageFlagBits::eColorAttachmentOutput; - } else { - log::fatal("Unsupported image layout transition."); - throw std::runtime_error("Unsupported image layout transition."); + // A wrapped (swapchain) image is handed to us by the acquire semaphore, so + // whatever we do next has to be ordered after that wait. + if (this->isExternal() && (this->layout == vk::ImageLayout::eUndefined || + this->layout == vk::ImageLayout::ePresentSrcKHR)) { + state.stage = SWAPCHAIN_ACQUIRE_STAGE; } - commandBuffer.pipelineBarrier(srcStage, dstStage, {}, nullptr, nullptr, - barrier); + return state; +} + +void Image::transitionLayout(vk::CommandBuffer commandBuffer, + vk::ImageLayout newLayout) { + AccessState from = this->getAccessState(); + AccessState to = getStateForLayout(newLayout); + + if (needsBarrier(from, to)) { + vk::ImageMemoryBarrier barrier = + makeImageBarrier(this->get(), this->getSubresourceRange(), from, to); + commandBuffer.pipelineBarrier(from.stage, to.stage, {}, nullptr, nullptr, + barrier); + } this->layout = newLayout; } diff --git a/engine/src/image.hpp b/engine/src/vulkan/image.hpp similarity index 88% rename from engine/src/image.hpp rename to engine/src/vulkan/image.hpp index b93095f..5f8647f 100644 --- a/engine/src/image.hpp +++ b/engine/src/vulkan/image.hpp @@ -4,6 +4,8 @@ #include <sophia/types.hpp> #include <vulkan/vulkan.hpp> +#include "vulkan/access.hpp" + namespace sophia { class Device; @@ -28,6 +30,20 @@ class Image { vk::Format getFormat() const { return this->format; } vk::Extent3D getExtent() const { return this->extent; } vk::ImageLayout getLayout() const { return this->layout; } + vk::ImageSubresourceRange getSubresourceRange() const; + + /** + * The layout this image was last left in, plus the stages and accesses that + * a barrier out of it has to wait on. + */ + AccessState getAccessState() const; + + /** + * Updates the tracked layout without recording a barrier. For code that + * does its own transitions (the RenderGraph) and needs to hand the final + * layout back. + */ + void recordLayout(vk::ImageLayout newLayout) { this->layout = newLayout; } void transitionLayout(vk::CommandBuffer commandBuffer, vk::ImageLayout newLayout); diff --git a/engine/src/pipelines/compute_pipeline.cpp b/engine/src/vulkan/pipelines/compute_pipeline.cpp similarity index 100% rename from engine/src/pipelines/compute_pipeline.cpp rename to engine/src/vulkan/pipelines/compute_pipeline.cpp diff --git a/engine/src/pipelines/compute_pipeline.hpp b/engine/src/vulkan/pipelines/compute_pipeline.hpp similarity index 92% rename from engine/src/pipelines/compute_pipeline.hpp rename to engine/src/vulkan/pipelines/compute_pipeline.hpp index 74e5e90..e8f7f43 100644 --- a/engine/src/pipelines/compute_pipeline.hpp +++ b/engine/src/vulkan/pipelines/compute_pipeline.hpp @@ -3,9 +3,9 @@ #include <memory> #include <vulkan/vulkan.hpp> -#include "device.hpp" #include "pipeline.hpp" -#include "shader.hpp" +#include "vulkan/device.hpp" +#include "vulkan/shader.hpp" namespace sophia { diff --git a/engine/src/pipelines/graphics_pipeline.cpp b/engine/src/vulkan/pipelines/graphics_pipeline.cpp similarity index 100% rename from engine/src/pipelines/graphics_pipeline.cpp rename to engine/src/vulkan/pipelines/graphics_pipeline.cpp diff --git a/engine/src/pipelines/graphics_pipeline.hpp b/engine/src/vulkan/pipelines/graphics_pipeline.hpp similarity index 98% rename from engine/src/pipelines/graphics_pipeline.hpp rename to engine/src/vulkan/pipelines/graphics_pipeline.hpp index 600253c..2a77943 100644 --- a/engine/src/pipelines/graphics_pipeline.hpp +++ b/engine/src/vulkan/pipelines/graphics_pipeline.hpp @@ -6,9 +6,9 @@ #include <vector> #include <vulkan/vulkan.hpp> -#include "device.hpp" #include "pipeline.hpp" -#include "shader.hpp" +#include "vulkan/device.hpp" +#include "vulkan/shader.hpp" namespace sophia { diff --git a/engine/src/pipelines/pipeline.cpp b/engine/src/vulkan/pipelines/pipeline.cpp similarity index 100% rename from engine/src/pipelines/pipeline.cpp rename to engine/src/vulkan/pipelines/pipeline.cpp diff --git a/engine/src/pipelines/pipeline.hpp b/engine/src/vulkan/pipelines/pipeline.hpp similarity index 95% rename from engine/src/pipelines/pipeline.hpp rename to engine/src/vulkan/pipelines/pipeline.hpp index 92b44fb..28f0b0d 100644 --- a/engine/src/pipelines/pipeline.hpp +++ b/engine/src/vulkan/pipelines/pipeline.hpp @@ -4,7 +4,7 @@ #include <vector> #include <vulkan/vulkan.hpp> -#include "device.hpp" +#include "vulkan/device.hpp" namespace sophia { diff --git a/engine/src/shader.cpp b/engine/src/vulkan/shader.cpp similarity index 95% rename from engine/src/shader.cpp rename to engine/src/vulkan/shader.cpp index 1093a52..e0de91b 100644 --- a/engine/src/shader.cpp +++ b/engine/src/vulkan/shader.cpp @@ -1,4 +1,4 @@ -#include "shader.hpp" +#include "vulkan/shader.hpp" #include <fstream> #include <shaderc/shaderc.hpp> @@ -74,9 +74,8 @@ void Shader::load(const std::string& path, ShaderStage shaderStage) { throw std::runtime_error("invalid SPIR-V binary: " + path); } - this->spirv.assign( - reinterpret_cast<const u32*>(bytes.data()), - reinterpret_cast<const u32*>(bytes.data() + bytes.size())); + this->spirv.assign(reinterpret_cast<const u32*>(bytes.data()), + reinterpret_cast<const u32*>(bytes.data() + bytes.size())); log::info("Loaded precompiled shader: " + path + " (" + std::to_string(bytes.size()) + " bytes)"); diff --git a/engine/src/shader.hpp b/engine/src/vulkan/shader.hpp similarity index 100% rename from engine/src/shader.hpp rename to engine/src/vulkan/shader.hpp diff --git a/engine/src/swapchain.cpp b/engine/src/vulkan/swapchain.cpp similarity index 99% rename from engine/src/swapchain.cpp rename to engine/src/vulkan/swapchain.cpp index ed07c2e..67744d9 100644 --- a/engine/src/swapchain.cpp +++ b/engine/src/vulkan/swapchain.cpp @@ -1,4 +1,4 @@ -#include "swapchain.hpp" +#include "vulkan/swapchain.hpp" #include <algorithm> #include <limits> diff --git a/engine/src/swapchain.hpp b/engine/src/vulkan/swapchain.hpp similarity index 94% rename from engine/src/swapchain.hpp rename to engine/src/vulkan/swapchain.hpp index 8168700..1a8b558 100644 --- a/engine/src/swapchain.hpp +++ b/engine/src/vulkan/swapchain.hpp @@ -5,8 +5,8 @@ #include <vector> #include <vulkan/vulkan.hpp> -#include "device.hpp" -#include "image.hpp" +#include "vulkan/device.hpp" +#include "vulkan/image.hpp" namespace sophia { @@ -23,9 +23,6 @@ class Swapchain { Image& getImage(int index) const { return *this->images.at(index); } Image& getDepthImage(int index) const { return *this->depthImages.at(index); } - vk::ImageView getImageView(int index) const { - return this->images.at(index)->getView(); - } vk::ImageView getDepthImageView(int index) const { return this->depthImages.at(index)->getView(); } diff --git a/examples/boids_2d/main.cpp b/examples/boids_2d/main.cpp index 1eaba8b..8dcaa57 100644 --- a/examples/boids_2d/main.cpp +++ b/examples/boids_2d/main.cpp @@ -1,6 +1,9 @@ #include <algorithm> #include <cmath> #include <cstdint> +#include <glm/glm.hpp> +#include <glm/gtc/constants.hpp> +#include <glm/gtc/matrix_transform.hpp> #include <iostream> #include <random> #include <sophia/application.hpp> @@ -10,12 +13,6 @@ #include <string> #include <vector> -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEPTH_ZERO_TO_ONE -#include <glm/glm.hpp> -#include <glm/gtc/constants.hpp> -#include <glm/gtc/matrix_transform.hpp> - struct Boid { glm::vec2 position; glm::vec2 velocity; @@ -91,7 +88,7 @@ class Boids2D : public sophia::Application { {{-BOID_LENGTH * 0.5f, BOID_WIDTH * 0.5f}, {1.0f, 1.0f, 1.0f}}, {{-BOID_LENGTH * 0.5f, -BOID_WIDTH * 0.5f}, {1.0f, 1.0f, 1.0f}}, }; - boidMesh = assets().loadMesh(std::move(vertices), {}); + boidMesh = resources().create<sophia::Mesh>(std::move(vertices)); } void onUpdate(const sophia::FrameInfo& frameInfo) override { diff --git a/testbed/main.cpp b/testbed/main.cpp index 05515d6..453c229 100644 --- a/testbed/main.cpp +++ b/testbed/main.cpp @@ -1,17 +1,41 @@ -#include <iostream> +#include <imgui.h> + #include <sophia/application.hpp> +#include <sophia/entity.hpp> +#include <sophia/frame_info.hpp> +#include <sophia/scene_view.hpp> +#include <sophia/ui/panel.hpp> + +class Viewport : public sophia::Panel { + public: + Viewport(sophia::UI& ui, sophia::RenderTargetHandle target) + : Panel{{.name = "Viewport"}}, ui{ui}, target{target} {} + + void onUpdate(const sophia::FrameInfo&) override { + ImGui::Text("Test ViewPort"); + + ui.image(target); + } + + private: + sophia::UI& ui; + sophia::RenderTargetHandle target; +}; class Testbed : public sophia::Application { public: Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {} void onCreate() override { - std::cout << "Testbed created" << std::endl; + view = views().create({.scene = scenes().create(), + .target = resources().create<sophia::RenderTarget>(), + .clearColor = {0.1f, 0.1f, 0.15f, 1.f}}); - debugUI().show(); + panels().emplace<Viewport>(ui(), views().get(view).target); } - void onUpdate(const sophia::FrameInfo& frameInfo) override {} + private: + sophia::SceneViewHandle view; }; SOPHIA_MAIN(Testbed)