Compare commits

...

27 Commits

Author SHA1 Message Date
burkkyy 97dc08890b Merge pull request 'Add offscreen render targets and rename AssetRegistry to ResourceRegistry' (#8) from dev into main
Reviewed-on: #8
2026-09-25 20:39:26 -07:00
burkkyy 6e12d8a710 changes 2026-09-25 20:39:04 -07:00
burkkyy 341249bb96 scene_view concept 2026-09-25 20:23:12 -07:00
burkkyy 0d145f615d changes 2026-09-25 20:22:29 -07:00
burkkyy 4ebae57cf8 Moving stuff around 2026-09-25 20:21:25 -07:00
burkkyy 09706507cf adding compiled shaders to project 2026-09-25 20:09:51 -07:00
burkkyy 1ccb591959 build system update 2026-09-25 19:58:11 -07:00
burkkyy 86b36f27c6 moving dir name 2026-09-25 19:32:33 -07:00
burkkyy 15932879e8 CLAUDE updates 2026-09-25 19:21:41 -07:00
burkkyy ebc0543535 better dev command 2026-09-25 19:04:01 -07:00
burkkyy 75fc14f4ac handy invalid handle macro 2026-09-24 23:00:45 -07:00
burkkyy b5d28ce3eb math.hpp added 2026-09-24 23:00:02 -07:00
burkkyy ae8db07d6f Simplified handles 2026-09-23 23:24:23 -07:00
burkkyy 97267f1a26 stuff 2026-09-24 05:31:38 +00:00
burkkyy 6a50cdb4e8 Formatting 2026-09-23 00:42:47 -07:00
burkkyy a98eac3dc7 laptop changes 2026-09-23 04:20:43 +00:00
burkkyy 39253771f1 starting scene renderer stuff 2026-09-21 11:34:29 -07:00
burkkyy ce54f2ffcc PM stuff 2026-09-21 01:33:39 -07:00
burkkyy 72e4ae79e2 gitea issue helper 2026-09-21 01:30:21 -07:00
burkkyy 4d037e1250 moving stuff around 2026-09-21 01:15:58 -07:00
burkkyy 650b4d813f moving files to better dirs 2026-09-21 01:10:13 -07:00
burkkyy 8fbabda095 Render Graph concept started! 2026-09-21 01:03:16 -07:00
burkkyy 2569eff130 changes 2026-09-21 00:30:11 -07:00
burkkyy f3c0f61eb1 Big changes 2026-09-21 00:26:50 -07:00
burkkyy 16f7bb7da3 Upgrading pool pattern 2026-09-20 20:52:09 -07:00
burkkyy fe40d4a629 Simply 2026-09-20 18:58:51 -07:00
burkkyy 5e1caabde6 Add offscreen render targets and rename AssetRegistry to ResourceRegistry
Add a RenderTarget resource so the scene can render offscreen instead of
straight to the swapchain. The editor uses it to show the scene inside the
Viewport panel.

- Rename AssetRegistry to ResourceRegistry (AssetTraits -> ResourceTraits,
  assets() -> resources()) and move the pools from engine/src/assets to
  engine/src/resources. It now covers non-asset GPU resources as well.
- Replace loadMesh() with create<T>(...), and add create<RenderTarget>(),
  requestResize(), getExtent() and isAlive().
- Add RenderTarget and RenderTargetPool with RenderTargetHandle.
  Resizes are deferred and applied at the start of the next frame, when the
  GPU is idle.
- Add Application::setSceneTarget(). With no target set, the scene still
  renders to the swapchain. With a target set, the scene renders offscreen
  and the swapchain receives only the UI. Frame extent and camera projection
  follow the target's size.
- Add sophia::UI (Application::ui()) with image() and getTextureId() to draw
  a render target in an ImGui window, sizing the target to the available
  space. ImGuiRenderer now registers the target textures for this.
- ViewportPanel takes the UI and a target, and draws the scene image in place
  of the flat background fill.
- Update the testbed, the boids example, and the glossary for the rename.
2026-09-20 16:21:22 -07:00
92 changed files with 2447 additions and 600 deletions
+31
View File
@@ -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.
+23
View File
@@ -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
+18
View File
@@ -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`
-1
View File
@@ -24,7 +24,6 @@ CMakeUserPresets.json
*.bin *.bin
*.out *.out
*.spv *.spv
*.spv.h
# Runtime data # Runtime data
pids pids
+2 -2
View File
@@ -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", "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" "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.useCMakePresets": "always",
"cmake.generator": "Ninja", "cmake.copyCompileCommands": "${workspaceFolder}/build/compile_commands.json",
"cmake.configureOnOpen": true, "cmake.configureOnOpen": true,
"files.associations": { "files.associations": {
".clang-format": "yaml", ".clang-format": "yaml",
+17
View File
@@ -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 "<title>" [-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.
+1 -3
View File
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.22.2) cmake_minimum_required(VERSION 3.25.2)
set(NAME sophia) set(NAME sophia)
@@ -15,8 +15,6 @@ endif()
if(CMAKE_BUILD_TYPE STREQUAL "Debug") if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message(STATUS "Debug build: enabling debug logging") message(STATUS "Debug build: enabling debug logging")
else()
add_compile_definitions(NDEBUG)
endif() endif()
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
+66
View File
@@ -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"
}
]
}
+8 -4
View File
@@ -8,17 +8,21 @@ COMING SOON
### Dependencies ### 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 - [Vulkan SDK](https://vulkan.lunarg.com/) - With validation layers
### Build instructions ### Build instructions
Builds use the presets in `CMakePresets.json` (`debug`, `release`), each in its own `build/<preset>` directory:
```sh ```sh
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug cmake --preset debug
cd build cmake --build --preset debug
make -j
``` ```
Or with the dev helper: `dev build [preset] [--fresh]`.
## Third Party libraries used ## Third Party libraries used
- [GLFW](https://github.com/glfw/glfw) - Window and input handling - [GLFW](https://github.com/glfw/glfw) - Window and input handling
+169 -5
View File
@@ -19,6 +19,8 @@ WAIT_FOR_PROCESS = "wait_for_process"
class DevHelper: class DevHelper:
SHADER_EXTENSIONS = (".vert", ".frag", ".comp", ".geom", ".tesc", ".tese") SHADER_EXTENSIONS = (".vert", ".frag", ".comp", ".geom", ".tesc", ".tese")
PROJECT_NAME = "sophia"
DEFAULT_PRESET = "debug"
def __init__(self): def __init__(self):
self.last_exit_status = 0 self.last_exit_status = 0
@@ -26,6 +28,9 @@ class DevHelper:
# Core logic # Core logic
def call(self, *args, **kwargs): def call(self, *args, **kwargs):
if not args: if not args:
print("Usage: dev <command> [args...]\n\nCommands:")
for command in self._commands():
print(f" {command}")
return return
command, rest = args[0], args[1:] command, rest = args[0], args[1:]
@@ -35,9 +40,29 @@ class DevHelper:
return method(*rest, **kwargs) return method(*rest, **kwargs)
# Primary command wrappers # 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): def build(self, *args, **kwargs):
command = f"rm -rf build && cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug" preset, fresh = self._parse_preset_args(args, "build")
self._exec_in_root(command, replace=True) 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. # Checks out a branch named after a Gitea issue URL.
@@ -61,6 +86,40 @@ class DevHelper:
flag = "" if exists else "-b " flag = "" if exists else "-b "
self._exec_in_root(f"git checkout {flag}{shlex.quote(branch_name)}", replace=True) 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): def renderdoc(self, *args, **kwargs):
qrenderdoc = self._find_qrenderdoc() qrenderdoc = self._find_qrenderdoc()
self._exec_in_root(qrenderdoc, replace=True) self._exec_in_root(qrenderdoc, replace=True)
@@ -101,14 +160,62 @@ class DevHelper:
raise NotImplementedError(f"Not implemented for platform {sys.platform}") raise NotImplementedError(f"Not implemented for platform {sys.platform}")
def bash_completions(self, *args, **kwargs): 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) METHOD_TO_COMMAND.get(name, name)
for name in vars(DevHelper) for name in vars(DevHelper)
if not name.startswith("_") and name != "call" and callable(getattr(DevHelper, name)) 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): def _parse_issue_url(self, url):
match = re.match(r"^(https?://[^/]+)/([^/]+)/([^/]+)/issues/(\d+)", url) match = re.match(r"^(https?://[^/]+)/([^/]+)/([^/]+)/issues/(\d+)", url)
if match is None: if match is None:
@@ -126,6 +233,63 @@ class DevHelper:
raise RuntimeError(f"failed to fetch issue title from {endpoint}") raise RuntimeError(f"failed to fetch issue title from {endpoint}")
return json.loads(result.stdout)["title"] 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): 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()))) 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(): if issue_repo == self._origin_repo():
+67 -17
View File
@@ -1,36 +1,56 @@
# Glossary # Glossary
## AssetRegistry ## ResourceRegistry
An AssetRegistry owns GPU-backed assets on the caller's behalf and hands out A ResourceRegistry owns GPU-backed resources on the caller's behalf and hands out
lightweight Handles instead of pointers or ownership. 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 Internally it's a collection of per-type pools, each a `Pool<T, Handle>`
asset type: (a generation-checked slot pool). A type that needs extra behaviour wraps its
pool in a thin class instead:
``` ```
AssetRegistry ResourceRegistry
├── MeshPool → MeshHandle ├── meshPool Pool<Mesh, MeshHandle>
├── TexturePool → TextureHandle (planned) ├── renderTargetPool RenderTargetPool → RenderTargetHandle
└── MaterialPool → MaterialHandle (planned) │ (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 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 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 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: Typical lifecycle:
``` ```
load<Mesh>(path) / loadMesh(...) → MeshHandle load<Mesh>(path) / create<Mesh>(...) → MeshHandle
│
get<Mesh>(handle) → const Mesh&
│ │
release<Mesh>(handle) 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?" lifetime directly?"
## Buffer ## 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 A PanelRegistry owns Panels on the caller's behalf and hands out lightweight
PanelHandles instead of pointers or ownership. PanelHandles instead of pointers or ownership.
Internally it's a generation-checked slot pool, the same shape AssetRegistry's Internally it wraps a `Pool<Panel, PanelHandle>` (the same generation-checked
per-type pools use: 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 PanelRegistry
└── Slot { Panel, generation } → PanelHandle └── Pool<Panel, PanelHandle>
└── Slot { Panel, generation } → PanelHandle
``` ```
Typical lifecycle: 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 Think: "Where do I register or look up a UI panel by handle, without owning
its lifetime directly?" 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 ## Sprite
A Sprite is a 2D renderable representation of an image, usually a rectangular region of a texture. A Sprite is a 2D renderable representation of an image, usually a rectangular region of a texture.
+4 -1
View File
@@ -18,7 +18,10 @@ class Editor : public sophia::Application {
panels().emplace<SceneHierarchyPanel>(); panels().emplace<SceneHierarchyPanel>();
panels().emplace<InspectorPanel>(); panels().emplace<InspectorPanel>();
panels().emplace<AssetBrowserPanel>(); 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( panels().setDefaultLayout(
{.left = {"Scene Hierarchy"}, {.left = {"Scene Hierarchy"},
+2 -1
View File
@@ -24,8 +24,9 @@ void ViewportPanel::onUpdate(const sophia::FrameInfo&) {
const ImVec2 size = ImGui::GetContentRegionAvail(); const ImVec2 size = ImGui::GetContentRegionAvail();
const ImVec2 end(origin.x + size.x, origin.y + size.y); const ImVec2 end(origin.x + size.x, origin.y + size.y);
ui.image(target);
ImDrawList* drawList = ImGui::GetWindowDrawList(); ImDrawList* drawList = ImGui::GetWindowDrawList();
drawList->AddRectFilled(origin, end, IM_COL32(24, 26, 32, 255));
if (showGrid) { if (showGrid) {
constexpr float GRID_STEP = 40.0f; constexpr float GRID_STEP = 40.0f;
+11 -1
View File
@@ -1,15 +1,25 @@
#pragma once #pragma once
#include <sophia/frame_info.hpp> #include <sophia/frame_info.hpp>
#include <sophia/handles.hpp>
#include <sophia/ui/panel.hpp> #include <sophia/ui/panel.hpp>
#include <sophia/ui/ui.hpp>
class ViewportPanel : public sophia::Panel { class ViewportPanel : public sophia::Panel {
public: 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; void onUpdate(const sophia::FrameInfo& frameInfo) override;
private: private:
sophia::UI& ui;
sophia::RenderTargetHandle target;
enum class GizmoMode { Move, Rotate, Scale }; enum class GizmoMode { Move, Rotate, Scale };
GizmoMode gizmoMode = GizmoMode::Move; GizmoMode gizmoMode = GizmoMode::Move;
+5
View File
@@ -17,6 +17,11 @@ target_include_directories(${ENGINE_NAME} PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/include" "${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 target_link_libraries(${ENGINE_NAME} PUBLIC
vulkan vulkan
shaderc shaderc
+12 -7
View File
@@ -3,21 +3,21 @@
#include <cstdlib> #include <cstdlib>
#include <iostream> #include <iostream>
#include <memory> #include <memory>
#include <sophia/asset_registry.hpp>
#include <sophia/camera_2d.hpp> #include <sophia/camera_2d.hpp>
#include <sophia/canvas.hpp> #include <sophia/canvas.hpp>
#include <sophia/console/console_command_registry.hpp> #include <sophia/console/console_command_registry.hpp>
#include <sophia/frame_info.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/types.hpp>
#include <sophia/ui/debug_ui.hpp> #include <sophia/ui/debug_ui.hpp>
#include <sophia/ui/panel_registry.hpp> #include <sophia/ui/panel_registry.hpp>
#include <sophia/ui/ui.hpp>
#include <string> #include <string>
#include <vector> #include <vector>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
namespace sophia { namespace sophia {
class Application { class Application {
@@ -44,8 +44,11 @@ class Application {
virtual void onRender() {} virtual void onRender() {}
virtual void onDestroy() {} virtual void onDestroy() {}
AssetRegistry& assets() { return this->assetRegistry; } ResourceRegistryRef resources();
PanelRegistry& panels() { return this->panelRegistry; } 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; } DebugUI& debugUI() { return this->_debugUI; }
@@ -66,8 +69,10 @@ class Application {
class Impl; class Impl;
std::unique_ptr<Impl> impl; std::unique_ptr<Impl> impl;
AssetRegistry assetRegistry;
PanelRegistry panelRegistry; PanelRegistry panelRegistry;
SceneRegistry sceneRegistry;
SceneViewRegistry sceneViewRegistry;
UI _ui;
DebugUI _debugUI; DebugUI _debugUI;
}; };
-64
View File
@@ -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
+1 -3
View File
@@ -1,9 +1,7 @@
#pragma once #pragma once
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
#include <sophia/math.hpp>
namespace sophia { namespace sophia {
+1 -4
View File
@@ -2,12 +2,9 @@
#include <memory> #include <memory>
#include <sophia/frame_info.hpp> #include <sophia/frame_info.hpp>
#include <sophia/math.hpp>
#include <vector> #include <vector>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
namespace sophia { namespace sophia {
class Application; class Application;
+52 -7
View File
@@ -1,18 +1,17 @@
#pragma once #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/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp> #include <iostream>
#include <ostream>
#include <sophia/handles.hpp>
#include <sophia/math.hpp>
#include <string>
namespace sophia { namespace sophia {
struct TagComponent { struct TagComponent {
std::string tag; // entt::hashed_string ? std::string tag; // entt::hashed_string ?
uint32_t mask = 0;
}; };
struct TransformComponent { struct TransformComponent {
@@ -28,10 +27,56 @@ struct TransformComponent {
glm::mat3 normalMatrix() const { glm::mat3 normalMatrix() const {
return glm::mat3{glm::transpose(glm::inverse(mat4()))}; 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 { struct MeshComponent {
MeshHandle handle; 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 } // namespace sophia
+62
View File
@@ -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
+1 -4
View File
@@ -1,12 +1,9 @@
#pragma once #pragma once
#include <sophia/camera_2d.hpp> #include <sophia/camera_2d.hpp>
#include <sophia/math.hpp>
#include <sophia/types.hpp> #include <sophia/types.hpp>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
namespace sophia { namespace sophia {
struct FrameInfo { struct FrameInfo {
+20 -5
View File
@@ -6,7 +6,7 @@
namespace sophia { namespace sophia {
template <typename Tag> template <typename T>
struct Handle { struct Handle {
static constexpr u32 INVALID_INDEX = (1u << 20) - 1; static constexpr u32 INVALID_INDEX = (1u << 20) - 1;
@@ -24,10 +24,25 @@ struct Handle {
[[nodiscard]] auto operator<=>(const Handle&) const = default; [[nodiscard]] auto operator<=>(const Handle&) const = default;
}; };
struct MeshTag {}; struct InvalidHandle {
struct PanelTag {}; template <typename T>
[[nodiscard]] constexpr operator Handle<T>() const {
return Handle<T>::invalid();
}
};
using MeshHandle = Handle<MeshTag>; inline constexpr InvalidHandle INVALID_HANDLE{};
using PanelHandle = Handle<PanelTag>;
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 } // namespace sophia
+17
View File
@@ -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
@@ -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{&registry} {}
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
+32
View File
@@ -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
+31
View File
@@ -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
+34
View File
@@ -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
@@ -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
+3 -3
View File
@@ -17,9 +17,9 @@ class DebugUI {
void show(); void show();
void hide(); void hide();
void showPanelRegistry(bool show = true); void showPanelRegistry(bool show = false);
void showFrameStats(bool show = true); void showFrameStats(bool show = false);
void showConsole(bool show = true); void showConsole(bool show = false);
private: private:
PanelRegistry& registry; PanelRegistry& registry;
+21 -35
View File
@@ -6,6 +6,7 @@
#include <sophia/ui/dock_layout.hpp> #include <sophia/ui/dock_layout.hpp>
#include <sophia/ui/panel.hpp> #include <sophia/ui/panel.hpp>
#include <sophia/util/assert.hpp> #include <sophia/util/assert.hpp>
#include <sophia/util/pool.hpp>
#include <string_view> #include <string_view>
#include <typeinfo> #include <typeinfo>
#include <utility> #include <utility>
@@ -21,17 +22,17 @@ class PanelRegistry {
PanelRegistry() = default; PanelRegistry() = default;
~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> template <typename T, typename... Args>
PanelHandle emplace(Args&&... args) { PanelHandle emplace(Args&&... args) {
return add(std::make_unique<T>(std::forward<Args>(args)...)); return add(std::make_unique<T>(std::forward<Args>(args)...));
} }
const Panel& get(PanelHandle handle) const; const Panel& get(PanelHandle handle) const { return pool.get(handle); }
Panel& get(PanelHandle handle) { Panel& get(PanelHandle handle) { return pool.get(handle); }
return const_cast<Panel&>(std::as_const(*this).get(handle));
}
template <typename T> template <typename T>
const T& get(PanelHandle handle) const { const T& get(PanelHandle handle) const {
@@ -46,19 +47,17 @@ class PanelRegistry {
return const_cast<T&>(std::as_const(*this).template get<T>(handle)); return const_cast<T&>(std::as_const(*this).template get<T>(handle));
} }
void remove(PanelHandle handle); void remove(PanelHandle handle) { pool.release(handle); }
bool isValid(PanelHandle handle) const; bool isValid(PanelHandle handle) const { return pool.isAlive(handle); }
template <typename T> template <typename T>
const T* find() const { const T* find() const {
for (const Slot& slot : slots) { const T* result = nullptr;
if (slot.panel) { pool.findIf([&result](const Panel& panel) {
if (const T* panel = dynamic_cast<const T*>(slot.panel.get())) { result = dynamic_cast<const T*>(&panel);
return panel; return result != nullptr;
} });
} return result;
}
return nullptr;
} }
template <typename T> template <typename T>
@@ -67,29 +66,22 @@ class PanelRegistry {
} }
const Panel* find(std::string_view name) const { const Panel* find(std::string_view name) const {
for (const Slot& slot : slots) { return pool.findIf(
if (slot.panel && slot.panel->getName() == name) { [name](const Panel& panel) { return panel.getName() == name; });
return slot.panel.get();
}
}
return nullptr;
} }
Panel* find(std::string_view name) { Panel* find(std::string_view name) {
return const_cast<Panel*>(std::as_const(*this).find(name)); return const_cast<Panel*>(std::as_const(*this).find(name));
} }
u32 size() const; u32 size() const { return pool.size(); }
u32 visibleCount() const; u32 visibleCount() const;
std::vector<std::string_view> names() const { std::vector<std::string_view> names() const {
std::vector<std::string_view> result; std::vector<std::string_view> result;
result.reserve(slots.size()); result.reserve(pool.size());
for (const Slot& slot : slots) { pool.forEach(
if (slot.panel) { [&result](const Panel& panel) { result.push_back(panel.getName()); });
result.push_back(slot.panel->getName());
}
}
return result; return result;
} }
@@ -101,13 +93,7 @@ class PanelRegistry {
const DockLayout& getDefaultLayout() const { return defaultLayout; } const DockLayout& getDefaultLayout() const { return defaultLayout; }
private: private:
struct Slot { Pool<Panel> pool{"Panel"};
std::unique_ptr<Panel> panel;
u32 generation = 0;
};
std::vector<Slot> slots;
std::vector<u32> freeList;
DockLayout defaultLayout; DockLayout defaultLayout;
}; };
+40
View File
@@ -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
+131
View File
@@ -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
+107 -60
View File
@@ -1,6 +1,11 @@
#include <imgui.h>
#include <algorithm>
#include <array> #include <array>
#include <chrono> #include <chrono>
#include <cstddef> #include <cstddef>
#include <glm/glm.hpp>
#include <map>
#include <sophia/application.hpp> #include <sophia/application.hpp>
#include <sophia/util/logger.hpp> #include <sophia/util/logger.hpp>
#include <string> #include <string>
@@ -8,20 +13,18 @@
#include <vector> #include <vector>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#define GLM_FORCE_RADIANS #include "rendering/imgui_renderer.hpp"
#define GLM_FORCE_DEPTH_ZERO_TO_ONE #include "rendering/model_render_system.hpp"
#include <imgui.h> #include "rendering/render_context.hpp"
#include "rendering/render_graph.hpp"
#include <glm/glm.hpp> #include "rendering/scene_renderer.hpp"
#include "resources/mesh.hpp"
#include "buffer.hpp" #include "resources/render_target.hpp"
#include "device.hpp" #include "resources/resource_registry.hpp"
#include "imgui_renderer.hpp" #include "vulkan/buffer.hpp"
#include "mesh.hpp" #include "vulkan/device.hpp"
#include "pipelines/graphics_pipeline.hpp" #include "vulkan/pipelines/graphics_pipeline.hpp"
#include "render_context.hpp" #include "vulkan/shader.hpp"
#include "shader.hpp"
#include "systems/model_render_system.hpp"
#include "window.hpp" #include "window.hpp"
namespace sophia { namespace sophia {
@@ -41,6 +44,12 @@ struct PushConstants {
std::vector<std::string> commandLineArgs; 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 } // namespace
class Application::Impl { class Application::Impl {
@@ -50,12 +59,17 @@ class Application::Impl {
config{config}, config{config},
window{config.width, config.height, config.name}, window{config.width, config.height, config.name},
device{&window}, device{&window},
renderContext{window, device} {} renderContext{window, device},
resourceRegistry{device, renderContext.getImageFormat()} {}
void run() { void run() {
ModelRenderSystem modelRenderSystem{device, renderContext}; ModelRenderSystem modelRenderSystem{device, renderContext};
SceneRenderer sceneRenderer{this->application.sceneRegistry,
this->resourceRegistry};
ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(), ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(),
renderContext.getImageCount()}; renderContext.getImageCount(),
this->resourceRegistry};
this->application._ui.renderer = &imguiRenderer;
this->application.consoleCommandRegistry.registerCommand( this->application.consoleCommandRegistry.registerCommand(
"reset_layout", "Restore the default panel layout", "reset_layout", "Restore the default panel layout",
@@ -65,8 +79,17 @@ class Application::Impl {
}); });
this->application.onCreate(); 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 startTime = std::chrono::high_resolution_clock::now();
auto currentTime = startTime; auto currentTime = startTime;
@@ -83,6 +106,10 @@ class Application::Impl {
double elapsedTime = double elapsedTime =
std::chrono::duration<double>(currentTime - startTime).count(); 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(); vk::Extent2D extent = renderContext.getExtent();
FrameInfo frameInfo{frameIndex, elapsedTime, deltaTime, FrameInfo frameInfo{frameIndex, elapsedTime, deltaTime,
glm::vec2(static_cast<float>(extent.width), glm::vec2(static_cast<float>(extent.width),
@@ -102,41 +129,9 @@ class Application::Impl {
imguiRenderer.beginFrame(); imguiRenderer.beginFrame();
this->application.panelRegistry.update(frameInfo); panelRegistry.update(frameInfo);
this->application.onRender(); 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; std::vector<ModelRenderSystem::MeshInstance> instances;
instances.reserve(this->meshes.size() + this->drawCalls.size()); instances.reserve(this->meshes.size() + this->drawCalls.size());
for (const std::unique_ptr<Mesh>& mesh : this->meshes) { for (const std::unique_ptr<Mesh>& mesh : this->meshes) {
@@ -144,16 +139,61 @@ class Application::Impl {
} }
instances.insert(instances.end(), this->drawCalls.begin(), instances.insert(instances.end(), this->drawCalls.begin(),
this->drawCalls.end()); this->drawCalls.end());
modelRenderSystem.render(commandBuffer, instances, frameInfo);
this->drawCalls.clear(); 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, auto [it, inserted] = viewTargets.try_emplace(view.target);
vk::ImageLayout::ePresentSrcKHR); 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(); renderContext.endFrame();
@@ -164,6 +204,8 @@ class Application::Impl {
device.waitIdle(); device.waitIdle();
this->application._ui.renderer = nullptr;
auto endTime = std::chrono::high_resolution_clock::now(); auto endTime = std::chrono::high_resolution_clock::now();
double totalRuntime = double totalRuntime =
std::chrono::duration<double>(endTime - startTime).count(); std::chrono::duration<double>(endTime - startTime).count();
@@ -193,7 +235,7 @@ class Application::Impl {
this->drawCalls.push_back({&mesh, transform}); this->drawCalls.push_back({&mesh, transform});
} }
Device& getDevice() { return device; } ResourceRegistry& getResourceRegistry() { return resourceRegistry; }
private: private:
Application& application; Application& application;
@@ -201,6 +243,7 @@ class Application::Impl {
Window window; Window window;
Device device; Device device;
RenderContext renderContext; RenderContext renderContext;
ResourceRegistry resourceRegistry;
std::vector<std::unique_ptr<Mesh>> meshes; std::vector<std::unique_ptr<Mesh>> meshes;
std::vector<ModelRenderSystem::MeshInstance> drawCalls; std::vector<ModelRenderSystem::MeshInstance> drawCalls;
@@ -214,13 +257,17 @@ Application::Application(const Config& config)
: impl{std::make_unique<Impl>(*this, config)}, : impl{std::make_unique<Impl>(*this, config)},
canvas{this}, canvas{this},
args{std::move(commandLineArgs)}, args{std::move(commandLineArgs)},
assetRegistry{impl->getDevice()}, _ui{ResourceRegistryRef{impl->getResourceRegistry()}},
_debugUI{this->panelRegistry, this->consoleCommandRegistry} {} _debugUI{this->panelRegistry, this->consoleCommandRegistry} {}
Application::~Application() = default; Application::~Application() = default;
void Application::run() { impl->run(); } void Application::run() { impl->run(); }
ResourceRegistryRef Application::resources() {
return ResourceRegistryRef{impl->getResourceRegistry()};
}
void Application::pushMesh(std::vector<CanvasVertex> vertices, void Application::pushMesh(std::vector<CanvasVertex> vertices,
std::vector<u32> indices) { std::vector<u32> indices) {
impl->pushMesh(std::move(vertices), std::move(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::clearMeshes() { impl->clearMeshes(); }
void Application::drawMesh(MeshHandle handle, const glm::mat4& transform) { 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 } // namespace sophia
-60
View File
@@ -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
-58
View File
@@ -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
-35
View File
@@ -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
@@ -1,10 +1,12 @@
#include "imgui_renderer.hpp" #include "rendering/imgui_renderer.hpp"
#include <imgui.h> #include <imgui.h>
#include <imgui_impl_glfw.h> #include <imgui_impl_glfw.h>
#include <imgui_impl_vulkan.h> #include <imgui_impl_vulkan.h>
#include <imgui_internal.h> #include <imgui_internal.h>
#include "resources/render_target.hpp"
namespace sophia { namespace sophia {
namespace { namespace {
@@ -20,7 +22,8 @@ void dockAll(const std::vector<std::string>& windows, ImGuiID nodeId) {
ImGuiRenderer::ImGuiRenderer(Window& window, Device& device, ImGuiRenderer::ImGuiRenderer(Window& window, Device& device,
vk::Format colorAttachmentFormat, vk::Format colorAttachmentFormat,
u32 minImageCount) { u32 minImageCount, ResourceRegistry& resources)
: resources{resources} {
// 1. Construct ImGui context // 1. Construct ImGui context
IMGUI_CHECKVERSION(); IMGUI_CHECKVERSION();
ImGui::CreateContext(); ImGui::CreateContext();
@@ -67,12 +70,17 @@ ImGuiRenderer::ImGuiRenderer(Window& window, Device& device,
} }
ImGuiRenderer::~ImGuiRenderer() { ImGuiRenderer::~ImGuiRenderer() {
for (const auto& [index, texture] : textures) {
ImGui_ImplVulkan_RemoveTexture(texture.descriptorSet);
}
ImGui_ImplVulkan_Shutdown(); ImGui_ImplVulkan_Shutdown();
ImGui_ImplGlfw_Shutdown(); ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext(); ImGui::DestroyContext();
} }
void ImGuiRenderer::beginFrame() { void ImGuiRenderer::beginFrame() {
releaseStaleTextures();
ImGui_ImplVulkan_NewFrame(); ImGui_ImplVulkan_NewFrame();
ImGui_ImplGlfw_NewFrame(); ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame(); ImGui::NewFrame();
@@ -82,6 +90,46 @@ void ImGuiRenderer::beginFrame() {
applyLayoutIfNeeded(dockspaceId, DOCKSPACE_FLAGS); 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) { void ImGuiRenderer::setDefaultLayout(DockLayout layout) {
defaultLayout = std::move(layout); defaultLayout = std::move(layout);
seedIfUnsaved = true; seedIfUnsaved = true;
@@ -2,10 +2,13 @@
#include <imgui.h> #include <imgui.h>
#include <sophia/handles.hpp>
#include <sophia/types.hpp> #include <sophia/types.hpp>
#include <sophia/ui/dock_layout.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" #include "window.hpp"
namespace sophia { namespace sophia {
@@ -16,17 +19,32 @@ class ImGuiRenderer {
ImGuiRenderer& operator=(const ImGuiRenderer&) = delete; ImGuiRenderer& operator=(const ImGuiRenderer&) = delete;
ImGuiRenderer(Window& window, Device& device, ImGuiRenderer(Window& window, Device& device,
vk::Format colorAttachmentFormat, u32 minImageCount); vk::Format colorAttachmentFormat, u32 minImageCount,
ResourceRegistry& resources);
~ImGuiRenderer(); ~ImGuiRenderer();
void beginFrame(); void beginFrame();
void endFrame(vk::CommandBuffer commandBuffer); void endFrame(vk::CommandBuffer commandBuffer);
ImTextureID getTextureId(RenderTargetHandle handle);
void setDefaultLayout(DockLayout layout); void setDefaultLayout(DockLayout layout);
void resetLayout(); void resetLayout();
private: private:
void applyLayoutIfNeeded(ImGuiID dockspaceId, ImGuiDockNodeFlags flags); 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; DockLayout defaultLayout;
bool seedIfUnsaved = false; bool seedIfUnsaved = false;
@@ -1,4 +1,4 @@
#include "model_render_system.hpp" #include "rendering/model_render_system.hpp"
#include <sophia/util/assert.hpp> #include <sophia/util/assert.hpp>
@@ -4,10 +4,10 @@
#include <sophia/frame_info.hpp> #include <sophia/frame_info.hpp>
#include <span> #include <span>
#include "device.hpp" #include "rendering/render_context.hpp"
#include "mesh.hpp" #include "resources/mesh.hpp"
#include "pipelines/graphics_pipeline.hpp" #include "vulkan/device.hpp"
#include "render_context.hpp" #include "vulkan/pipelines/graphics_pipeline.hpp"
namespace sophia { namespace sophia {
@@ -1,10 +1,12 @@
#include "render_context.hpp" #include "rendering/render_context.hpp"
#include <limits> #include <limits>
#include <sophia/util/assert.hpp> #include <sophia/util/assert.hpp>
#include <sophia/util/logger.hpp> #include <sophia/util/logger.hpp>
#include <stdexcept> #include <stdexcept>
#include "vulkan/access.hpp"
namespace sophia { namespace sophia {
RenderContext::RenderContext(Window& window, Device& device) RenderContext::RenderContext(Window& window, Device& device)
@@ -80,8 +82,7 @@ void RenderContext::endFrame() {
vk::Semaphore waitSemaphore = frame.imageAvailable.get(); vk::Semaphore waitSemaphore = frame.imageAvailable.get();
vk::Semaphore signalSemaphore = vk::Semaphore signalSemaphore =
this->renderFinishedSemaphores.at(this->currentImageIndex).get(); this->renderFinishedSemaphores.at(this->currentImageIndex).get();
vk::PipelineStageFlags waitStage = vk::PipelineStageFlags waitStage = SWAPCHAIN_ACQUIRE_STAGE;
vk::PipelineStageFlagBits::eColorAttachmentOutput;
vk::CommandBuffer commandBuffer = frame.commandBuffer; vk::CommandBuffer commandBuffer = frame.commandBuffer;
vk::SubmitInfo submitInfo{}; vk::SubmitInfo submitInfo{};
@@ -6,8 +6,8 @@
#include <vector> #include <vector>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include "device.hpp" #include "vulkan/device.hpp"
#include "swapchain.hpp" #include "vulkan/swapchain.hpp"
#include "window.hpp" #include "window.hpp"
namespace sophia { namespace sophia {
@@ -31,14 +31,8 @@ class RenderContext {
vk::CommandBuffer beginFrame(); vk::CommandBuffer beginFrame();
void endFrame(); void endFrame();
void transitionCurrentImage(vk::CommandBuffer commandBuffer, Image& getCurrentImage() const {
vk::ImageLayout newLayout) const { return this->swapchain->getImage(static_cast<int>(this->currentImageIndex));
this->swapchain->getImage(static_cast<int>(this->currentImageIndex))
.transitionLayout(commandBuffer, newLayout);
}
vk::ImageView getCurrentImageView() const {
return this->swapchain->getImageView(
static_cast<int>(this->currentImageIndex));
} }
vk::Extent2D getExtent() const { return this->swapchain->getExtent(); } vk::Extent2D getExtent() const { return this->swapchain->getExtent(); }
+168
View File
@@ -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
+149
View File
@@ -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
+74
View File
@@ -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
+29
View File
@@ -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
@@ -1,4 +1,4 @@
#include "mesh.hpp" #include "resources/mesh.hpp"
#include <sophia/util/assert.hpp> #include <sophia/util/assert.hpp>
#include <sophia/util/logger.hpp> #include <sophia/util/logger.hpp>
@@ -1,23 +1,18 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include <sophia/asset_registry.hpp> #include <sophia/handles.hpp>
#include <sophia/math.hpp>
#include <sophia/types.hpp> #include <sophia/types.hpp>
#include <vector> #include <vector>
#define GLM_FORCE_RADIANS #include "vulkan/buffer.hpp"
#define GLM_FORCE_DEPTH_ZERO_TO_ONE #include "vulkan/device.hpp"
#include <glm/glm.hpp>
#include "buffer.hpp"
#include "device.hpp"
namespace sophia { namespace sophia {
class Mesh { class Mesh {
public: public:
using HandleType = MeshHandle;
struct Vertex { struct Vertex {
glm::vec2 position{}; glm::vec2 position{};
glm::vec3 color{}; glm::vec3 color{};
@@ -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
@@ -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
+52
View File
@@ -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
+51
View File
@@ -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
@@ -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
@@ -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
-16
View File
@@ -1,16 +0,0 @@
#pragma once
#include <entt/entt.hpp>
namespace sophia {
class Scene {
public:
Scene();
~Scene();
private:
entt::registry registry;
};
} // namespace sophia
+8
View File
@@ -0,0 +1,8 @@
#include <sophia/entity.hpp>
namespace sophia {
Entity::Entity(entt::entity handle, Scene* scene)
: handle(handle), scene(scene) {}
} // namespace sophia
@@ -1,6 +1,5 @@
#include "scene.hpp"
#include <sophia/components.hpp> #include <sophia/components.hpp>
#include <sophia/scene.hpp>
#include <sophia/util/logger.hpp> #include <sophia/util/logger.hpp>
namespace sophia { namespace sophia {
+100
View File
@@ -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
@@ -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
@@ -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
+1
View File
@@ -12,6 +12,7 @@ DebugUI::DebugUI(PanelRegistry& registry, ConsoleCommandRegistry& commands)
this->panelRegistryHandle = registry.emplace<PanelRegistryPanel>(registry); this->panelRegistryHandle = registry.emplace<PanelRegistryPanel>(registry);
this->frameStatsHandle = registry.emplace<FrameStatsPanel>(); this->frameStatsHandle = registry.emplace<FrameStatsPanel>();
this->consoleHandle = registry.emplace<ConsolePanel>(commands); this->consoleHandle = registry.emplace<ConsolePanel>(commands);
hide();
// #endif // !NDEBUG // #endif // !NDEBUG
} }
+7 -72
View File
@@ -1,88 +1,23 @@
#include <sophia/ui/panel_registry.hpp> #include <sophia/ui/panel_registry.hpp>
#include <sophia/util/assert.hpp>
namespace sophia { 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 PanelRegistry::visibleCount() const {
u32 count = 0; u32 count = 0;
for (const Slot& slot : slots) { pool.forEach([&count](const Panel& panel) {
if (slot.panel && slot.panel->isVisible()) { if (panel.isVisible()) {
count++; count++;
} }
} });
return count; return count;
} }
void PanelRegistry::update(const FrameInfo& frameInfo) { void PanelRegistry::update(const FrameInfo& frameInfo) {
for (auto& slot : slots) { pool.forEach([&frameInfo](Panel& panel) {
if (slot.panel && slot.panel->isVisible()) { if (panel.isVisible()) {
slot.panel->draw(frameInfo); panel.draw(frameInfo);
} }
} });
} }
} // namespace sophia } // namespace sophia
+26
View File
@@ -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
+104
View File
@@ -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
+63
View File
@@ -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
@@ -2,7 +2,7 @@
* Based on * Based on
* https://github.com/blurrypiano/littleVulkanEngine/blob/main/src/lve_buffer.cpp * https://github.com/blurrypiano/littleVulkanEngine/blob/main/src/lve_buffer.cpp
*/ */
#include "buffer.hpp" #include "vulkan/buffer.hpp"
#include <sophia/util/assert.hpp> #include <sophia/util/assert.hpp>
@@ -4,7 +4,7 @@
*/ */
#pragma once #pragma once
#include "device.hpp" #include "vulkan/device.hpp"
namespace sophia { namespace sophia {
+34
View File
@@ -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
+13
View File
@@ -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
@@ -3,7 +3,7 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
#include "device.hpp" #include "vulkan/device.hpp"
namespace sophia { namespace sophia {
@@ -4,7 +4,7 @@
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include "device.hpp" #include "vulkan/device.hpp"
namespace sophia { namespace sophia {
@@ -4,7 +4,7 @@
#include "descriptor_pool.hpp" #include "descriptor_pool.hpp"
#include "descriptor_set_layout.hpp" #include "descriptor_set_layout.hpp"
#include "device.hpp" #include "vulkan/device.hpp"
namespace sophia { namespace sophia {
@@ -1,10 +1,12 @@
#include "device.hpp" #include "vulkan/device.hpp"
#include <string.h> #include <string.h>
#include <set> #include <set>
#include <sophia/util/logger.hpp> #include <sophia/util/logger.hpp>
#include "vulkan/debug_label.hpp"
namespace sophia { namespace sophia {
static VKAPI_ATTR vk::Bool32 VKAPI_CALL static VKAPI_ATTR vk::Bool32 VKAPI_CALL
@@ -288,6 +290,8 @@ void Device::setupDebugMessenger() {
nullptr, &this->debugMessenger) != VK_SUCCESS) { nullptr, &this->debugMessenger) != VK_SUCCESS) {
throw std::runtime_error("failed to set up debug callback!"); throw std::runtime_error("failed to set up debug callback!");
} }
loadDebugLabelFunctions(*instance);
} }
bool Device::checkValidationLayerSupport() { bool Device::checkValidationLayerSupport() {
@@ -1,81 +1,42 @@
#include "image.hpp" #include "vulkan/image.hpp"
#include <cstring> #include <cstring>
#include <sophia/util/logger.hpp> #include <sophia/util/logger.hpp>
#include <stdexcept> #include <stdexcept>
#include "device.hpp" #include "vulkan/device.hpp"
namespace sophia { namespace sophia {
void Image::transitionLayout(vk::CommandBuffer commandBuffer, vk::ImageSubresourceRange Image::getSubresourceRange() const {
vk::ImageLayout newLayout) { return vk::ImageSubresourceRange{this->aspectMask, 0, this->mipLevels, 0,
vk::ImageMemoryBarrier barrier{}; this->arrayLayers};
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::PipelineStageFlags srcStage; AccessState Image::getAccessState() const {
vk::PipelineStageFlags dstStage; AccessState state = getStateForLayout(this->layout);
if (this->layout == vk::ImageLayout::eUndefined && // A wrapped (swapchain) image is handed to us by the acquire semaphore, so
newLayout == vk::ImageLayout::eTransferDstOptimal) { // whatever we do next has to be ordered after that wait.
barrier.srcAccessMask = {}; if (this->isExternal() && (this->layout == vk::ImageLayout::eUndefined ||
barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite; this->layout == vk::ImageLayout::ePresentSrcKHR)) {
srcStage = vk::PipelineStageFlagBits::eTopOfPipe; state.stage = SWAPCHAIN_ACQUIRE_STAGE;
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.");
} }
commandBuffer.pipelineBarrier(srcStage, dstStage, {}, nullptr, nullptr, return state;
barrier); }
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; this->layout = newLayout;
} }
@@ -4,6 +4,8 @@
#include <sophia/types.hpp> #include <sophia/types.hpp>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include "vulkan/access.hpp"
namespace sophia { namespace sophia {
class Device; class Device;
@@ -28,6 +30,20 @@ class Image {
vk::Format getFormat() const { return this->format; } vk::Format getFormat() const { return this->format; }
vk::Extent3D getExtent() const { return this->extent; } vk::Extent3D getExtent() const { return this->extent; }
vk::ImageLayout getLayout() const { return this->layout; } 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, void transitionLayout(vk::CommandBuffer commandBuffer,
vk::ImageLayout newLayout); vk::ImageLayout newLayout);
@@ -3,9 +3,9 @@
#include <memory> #include <memory>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include "device.hpp"
#include "pipeline.hpp" #include "pipeline.hpp"
#include "shader.hpp" #include "vulkan/device.hpp"
#include "vulkan/shader.hpp"
namespace sophia { namespace sophia {
@@ -6,9 +6,9 @@
#include <vector> #include <vector>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include "device.hpp"
#include "pipeline.hpp" #include "pipeline.hpp"
#include "shader.hpp" #include "vulkan/device.hpp"
#include "vulkan/shader.hpp"
namespace sophia { namespace sophia {
@@ -4,7 +4,7 @@
#include <vector> #include <vector>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include "device.hpp" #include "vulkan/device.hpp"
namespace sophia { namespace sophia {
@@ -1,4 +1,4 @@
#include "shader.hpp" #include "vulkan/shader.hpp"
#include <fstream> #include <fstream>
#include <shaderc/shaderc.hpp> #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); throw std::runtime_error("invalid SPIR-V binary: " + path);
} }
this->spirv.assign( this->spirv.assign(reinterpret_cast<const u32*>(bytes.data()),
reinterpret_cast<const u32*>(bytes.data()), reinterpret_cast<const u32*>(bytes.data() + bytes.size()));
reinterpret_cast<const u32*>(bytes.data() + bytes.size()));
log::info("Loaded precompiled shader: " + path + " (" + log::info("Loaded precompiled shader: " + path + " (" +
std::to_string(bytes.size()) + " bytes)"); std::to_string(bytes.size()) + " bytes)");
@@ -1,4 +1,4 @@
#include "swapchain.hpp" #include "vulkan/swapchain.hpp"
#include <algorithm> #include <algorithm>
#include <limits> #include <limits>
@@ -5,8 +5,8 @@
#include <vector> #include <vector>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include "device.hpp" #include "vulkan/device.hpp"
#include "image.hpp" #include "vulkan/image.hpp"
namespace sophia { namespace sophia {
@@ -23,9 +23,6 @@ class Swapchain {
Image& getImage(int index) const { return *this->images.at(index); } Image& getImage(int index) const { return *this->images.at(index); }
Image& getDepthImage(int index) const { return *this->depthImages.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 { vk::ImageView getDepthImageView(int index) const {
return this->depthImages.at(index)->getView(); return this->depthImages.at(index)->getView();
} }
+4 -7
View File
@@ -1,6 +1,9 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <glm/glm.hpp>
#include <glm/gtc/constants.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <iostream> #include <iostream>
#include <random> #include <random>
#include <sophia/application.hpp> #include <sophia/application.hpp>
@@ -10,12 +13,6 @@
#include <string> #include <string>
#include <vector> #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 { struct Boid {
glm::vec2 position; glm::vec2 position;
glm::vec2 velocity; 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}},
{{-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 { void onUpdate(const sophia::FrameInfo& frameInfo) override {
+28 -4
View File
@@ -1,17 +1,41 @@
#include <iostream> #include <imgui.h>
#include <sophia/application.hpp> #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 { class Testbed : public sophia::Application {
public: public:
Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {} Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {}
void onCreate() override { 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) SOPHIA_MAIN(Testbed)