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
*.out
*.spv
*.spv.h
# Runtime data
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",
"VK_ADD_LAYER_PATH": "/home/caleb/.local/share/vulkan-sdk/1.4.357.1/x86_64/share/vulkan/explicit_layer.d"
},
"cmake.buildDirectory": "${workspaceFolder}/build",
"cmake.generator": "Ninja",
"cmake.useCMakePresets": "always",
"cmake.copyCompileCommands": "${workspaceFolder}/build/compile_commands.json",
"cmake.configureOnOpen": true,
"files.associations": {
".clang-format": "yaml",
+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)
@@ -15,8 +15,6 @@ endif()
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message(STATUS "Debug build: enabling debug logging")
else()
add_compile_definitions(NDEBUG)
endif()
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
+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
- [cmake](https://cmake.org/)
- [cmake](https://cmake.org/) 3.25+
- [Ninja](https://ninja-build.org/)
- [Vulkan SDK](https://vulkan.lunarg.com/) - With validation layers
### Build instructions
Builds use the presets in `CMakePresets.json` (`debug`, `release`), each in its own `build/<preset>` directory:
```sh
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cd build
make -j
cmake --preset debug
cmake --build --preset debug
```
Or with the dev helper: `dev build [preset] [--fresh]`.
## Third Party libraries used
- [GLFW](https://github.com/glfw/glfw) - Window and input handling
+169 -5
View File
@@ -19,6 +19,8 @@ WAIT_FOR_PROCESS = "wait_for_process"
class DevHelper:
SHADER_EXTENSIONS = (".vert", ".frag", ".comp", ".geom", ".tesc", ".tese")
PROJECT_NAME = "sophia"
DEFAULT_PRESET = "debug"
def __init__(self):
self.last_exit_status = 0
@@ -26,6 +28,9 @@ class DevHelper:
# Core logic
def call(self, *args, **kwargs):
if not args:
print("Usage: dev <command> [args...]\n\nCommands:")
for command in self._commands():
print(f" {command}")
return
command, rest = args[0], args[1:]
@@ -35,9 +40,29 @@ class DevHelper:
return method(*rest, **kwargs)
# Primary command wrappers
##
# Configures and builds a CMake preset (see CMakePresets.json).
# dev build [preset] [--fresh]
# The preset defaults to debug and builds into build/<preset>. --fresh
# discards the preset's CMake cache before configuring.
def build(self, *args, **kwargs):
command = f"rm -rf build && cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug"
self._exec_in_root(command, replace=True)
preset, fresh = self._parse_preset_args(args, "build")
self._build_preset(preset, fresh)
##
# Builds the debug preset (incrementally), then runs the testbed from
# build/debug. Any arguments are passed through to the testbed.
# dev testbed [args...]
def testbed(self, *args, **kwargs):
preset = self.DEFAULT_PRESET
self._build_preset(preset, fresh=False)
preset_dir = os.path.join(self._project_root(), "build", preset)
binary = os.path.join(preset_dir, "bin", "testbed")
print(f"Running: {shlex.join([binary, *args])}")
os.chdir(preset_dir)
os.execv(binary, [binary, *args])
##
# Checks out a branch named after a Gitea issue URL.
@@ -61,6 +86,40 @@ class DevHelper:
flag = "" if exists else "-b "
self._exec_in_root(f"git checkout {flag}{shlex.quote(branch_name)}", replace=True)
##
# Creates an issue on this project's Gitea repo.
# dev issue <title> [-b|--body <text>] [-l|--label <name>]... [-a|--assignee <user>]... [-o|--open]
# The body is read from stdin if --body is not given and stdin is piped.
# Gitea's API cannot add issues to a project (only the web UI can), so the
# issue URL is printed and --open opens it in the browser to do so.
def issue(self, *args, **kwargs):
title, body, labels, assignees, open_in_browser = self._parse_issue_args(args)
repo = self._origin_repo()
if repo is None:
raise RuntimeError("could not determine the Gitea repo from the origin remote")
payload = {"title": title, "body": body}
if labels:
label_ids = self._fetch_label_ids(repo)
unknown = [label for label in labels if label not in label_ids]
if unknown:
raise ValueError(f"Unknown label(s): {', '.join(unknown)} (available: {', '.join(sorted(label_ids))})")
payload["labels"] = [label_ids[label] for label in labels]
if assignees:
payload["assignees"] = assignees
issue = self._tea_api(f"repos/{repo}/issues", payload)
print(f"Created issue #{issue['number']}: {issue['title']}")
print(issue["html_url"])
print(f"Add it to the project from the issue sidebar: Projects > {self.PROJECT_NAME}")
if open_in_browser:
opener = shutil.which("xdg-open") or shutil.which("open")
if opener is None:
raise RuntimeError("xdg-open not found on PATH")
subprocess.Popen([opener, issue["html_url"]], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def renderdoc(self, *args, **kwargs):
qrenderdoc = self._find_qrenderdoc()
self._exec_in_root(qrenderdoc, replace=True)
@@ -101,14 +160,62 @@ class DevHelper:
raise NotImplementedError(f"Not implemented for platform {sys.platform}")
def bash_completions(self, *args, **kwargs):
completions = sorted(
print(" ".join(self._commands()))
# Private helpers
def _commands(self):
return sorted(
METHOD_TO_COMMAND.get(name, name)
for name in vars(DevHelper)
if not name.startswith("_") and name != "call" and callable(getattr(DevHelper, name))
)
print(" ".join(completions))
# Private helpers
def _parse_preset_args(self, args, command):
preset = None
fresh = False
for arg in args:
if arg == "--fresh":
fresh = True
elif arg.startswith("-"):
raise ValueError(f"Unknown option: {arg}")
elif preset is None:
preset = arg
else:
raise ValueError(f"Usage: dev {command} [preset] [--fresh]")
preset = preset or self.DEFAULT_PRESET
presets = self._configure_presets()
if preset not in presets:
raise ValueError(f"Unknown preset: {preset} (available: {', '.join(presets)})")
return preset, fresh
def _configure_presets(self):
with open(os.path.join(self._project_root(), "CMakePresets.json")) as f:
presets = json.load(f)["configurePresets"]
return [preset["name"] for preset in presets if not preset.get("hidden")]
def _build_preset(self, preset, fresh):
configure = ["cmake", "--preset", preset]
if fresh:
configure.append("--fresh")
self._run_in_root(configure)
self._copy_compile_commands(preset)
self._run_in_root(["cmake", "--build", "--preset", preset])
# Editor tooling reads build/compile_commands.json, so point it at the
# most recently configured preset.
def _copy_compile_commands(self, preset):
build_dir = os.path.join(self._project_root(), "build")
source = os.path.join(build_dir, preset, "compile_commands.json")
if os.path.isfile(source):
shutil.copyfile(source, os.path.join(build_dir, "compile_commands.json"))
def _run_in_root(self, command):
print(f"Running: {shlex.join(command)}")
result = subprocess.run(command, cwd=self._project_root())
if result.returncode != 0:
sys.exit(result.returncode)
def _parse_issue_url(self, url):
match = re.match(r"^(https?://[^/]+)/([^/]+)/([^/]+)/issues/(\d+)", url)
if match is None:
@@ -126,6 +233,63 @@ class DevHelper:
raise RuntimeError(f"failed to fetch issue title from {endpoint}")
return json.loads(result.stdout)["title"]
def _parse_issue_args(self, args):
title = None
body = None
labels = []
assignees = []
open_in_browser = False
args = list(args)
while args:
arg = args.pop(0)
if arg in ("-b", "--body", "-l", "--label", "-a", "--assignee"):
if not args:
raise ValueError(f"{arg} requires a value")
value = args.pop(0)
if arg in ("-b", "--body"):
body = value
elif arg in ("-l", "--label"):
labels.append(value)
else:
assignees.append(value)
elif arg in ("-o", "--open"):
open_in_browser = True
elif arg.startswith("-"):
raise ValueError(f"Unknown option: {arg}")
elif title is None:
title = arg
else:
title = f"{title} {arg}"
if not title:
raise ValueError("Must provide an issue title. Usage: dev issue <title> [-b <body>] [-l <label>]... [-a <user>]... [-o]")
if body is None:
body = "" if sys.stdin.isatty() else sys.stdin.read()
return title, body, labels, assignees, open_in_browser
def _tea_api(self, endpoint, payload=None):
if shutil.which("tea") is None:
raise RuntimeError("tea not found on PATH (install gitea tea and run `tea login add`)")
command = ["tea", "api", endpoint]
if payload is not None:
command += ["-d", "@-"]
print(f"Running: {' '.join(command)}")
result = subprocess.run(
command,
input=json.dumps(payload) if payload is not None else None,
stdout=subprocess.PIPE,
text=True,
)
if result.returncode != 0:
raise RuntimeError(f"tea api request failed: {endpoint}")
return json.loads(result.stdout)
def _fetch_label_ids(self, repo):
labels = self._tea_api(f"repos/{repo}/labels?limit=100")
return {label["name"]: label["id"] for label in labels}
def _format_branch_name(self, issue_repo, issue_number, issue_title):
formatted_title = re.sub(r"-+", "-", re.sub(r"[^a-z0-9\-]", "", re.sub(r"\s+", "-", issue_title.lower().strip())))
if issue_repo == self._origin_repo():
+66 -16
View File
@@ -1,36 +1,56 @@
# Glossary
## AssetRegistry
## ResourceRegistry
An AssetRegistry owns GPU-backed assets on the caller's behalf and hands out
lightweight Handles instead of pointers or ownership.
A ResourceRegistry owns GPU-backed resources on the caller's behalf and hands out
lightweight Handles instead of pointers or ownership. It is engine-internal:
frontend code never sees it, and reaches it through a ResourceRegistryRef.
Internally it's a collection of per-type pools, each responsible for one
asset type:
Internally it's a collection of per-type pools, each a `Pool<T, Handle>`
(a generation-checked slot pool). A type that needs extra behaviour wraps its
pool in a thin class instead:
```
AssetRegistry
├── MeshPool → MeshHandle
├── TexturePool → TextureHandle (planned)
└── MaterialPool → MaterialHandle (planned)
ResourceRegistry
├── meshPool Pool<Mesh, MeshHandle>
├── renderTargetPool RenderTargetPool → RenderTargetHandle
│ (wraps Pool; waits for the GPU on release, applies resizes)
├── texturePool (planned)
└── materialPool (planned)
```
Engine code (`Application`, `ImGuiRenderer`, `SceneRenderer`) uses the pools
directly, e.g. `resources.renderTargetPool.get(handle)`.
A Handle is a small (index, generation) pair. The index locates the slot in
the pool; the generation is bumped when that slot is released, so a Handle
kept around after release() is detected as stale instead of resolving to
someone else's asset (or freed memory).
someone else's resource (or freed memory).
Think: "Where does the engine keep every GPU resource, and how does it look one
up by handle?"
## ResourceRegistryRef
A ResourceRegistryRef is the frontend's non-owning, cheap-to-copy view of the
ResourceRegistry, returned by `Application::resources()`. It exposes only what
frontend code needs and hands out no pools, so no Vulkan-facing types leak
through the public API:
```
create<T>(...) load<T>(path) release<T>(handle)
requestResize(target, w, h) getExtent(target) isAlive(handle)
```
Typical lifecycle:
```
load<Mesh>(path) / loadMesh(...) → MeshHandle
│
get<Mesh>(handle) → const Mesh&
load<Mesh>(path) / create<Mesh>(...) → MeshHandle
│
release<Mesh>(handle)
```
Think: "Where do I get or release a GPU asset by handle, without owning its
Think: "How do I create or release a GPU resource by handle, without owning its
lifetime directly?"
## Buffer
@@ -256,11 +276,13 @@ This is primarily a UI concept, rather than a GPU concept.
A PanelRegistry owns Panels on the caller's behalf and hands out lightweight
PanelHandles instead of pointers or ownership.
Internally it's a generation-checked slot pool, the same shape AssetRegistry's
per-type pools use:
Internally it wraps a `Pool<Panel, PanelHandle>` (the same generation-checked
slot pool ResourceRegistry uses, in `sophia/util/pool.hpp`) and adds the
Panel-specific parts: lookup by type or name, and the default dock layout:
```
PanelRegistry
└── Pool<Panel, PanelHandle>
└── Slot { Panel, generation } → PanelHandle
```
@@ -279,6 +301,34 @@ Each frame, update(frameInfo) calls onUpdate on every visible Panel it holds.
Think: "Where do I register or look up a UI panel by handle, without owning
its lifetime directly?"
## SceneRegistry
A SceneRegistry owns Scenes on the caller's behalf and hands out lightweight
SceneHandles instead of pointers or ownership. Like PanelRegistry, it is owned
by Application and reached from frontend code directly (`scenes()`), rather than
through ResourceRegistryRef: scenes are CPU-side entity data, not GPU resources.
Internally it wraps a `Pool<Scene, SceneHandle>`:
```
SceneRegistry
└── Pool<Scene, SceneHandle>
└── Slot { Scene, generation } → SceneHandle
```
Typical lifecycle:
```
create() → SceneHandle
│
get(handle) → Scene&
│
release(handle)
```
Think: "Where do I create or look up a Scene by handle, without owning its
lifetime directly?"
## Sprite
A Sprite is a 2D renderable representation of an image, usually a rectangular region of a texture.
+4 -1
View File
@@ -18,7 +18,10 @@ class Editor : public sophia::Application {
panels().emplace<SceneHierarchyPanel>();
panels().emplace<InspectorPanel>();
panels().emplace<AssetBrowserPanel>();
panels().emplace<ViewportPanel>();
sophia::RenderTargetHandle sceneTarget =
resources().create<sophia::RenderTarget>();
views().create({.scene = scenes().create(), .target = sceneTarget});
panels().emplace<ViewportPanel>(ui(), sceneTarget);
panels().setDefaultLayout(
{.left = {"Scene Hierarchy"},
+2 -1
View File
@@ -24,8 +24,9 @@ void ViewportPanel::onUpdate(const sophia::FrameInfo&) {
const ImVec2 size = ImGui::GetContentRegionAvail();
const ImVec2 end(origin.x + size.x, origin.y + size.y);
ui.image(target);
ImDrawList* drawList = ImGui::GetWindowDrawList();
drawList->AddRectFilled(origin, end, IM_COL32(24, 26, 32, 255));
if (showGrid) {
constexpr float GRID_STEP = 40.0f;
+11 -1
View File
@@ -1,15 +1,25 @@
#pragma once
#include <sophia/frame_info.hpp>
#include <sophia/handles.hpp>
#include <sophia/ui/panel.hpp>
#include <sophia/ui/ui.hpp>
class ViewportPanel : public sophia::Panel {
public:
ViewportPanel() : Panel{{.name = "Viewport"}} {}
ViewportPanel(sophia::UI& ui, sophia::RenderTargetHandle target)
: Panel{{.name = "Viewport",
.windowFlags = ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse}},
ui{ui},
target{target} {}
void onUpdate(const sophia::FrameInfo& frameInfo) override;
private:
sophia::UI& ui;
sophia::RenderTargetHandle target;
enum class GizmoMode { Move, Rotate, Scale };
GizmoMode gizmoMode = GizmoMode::Move;
+5
View File
@@ -17,6 +17,11 @@ target_include_directories(${ENGINE_NAME} PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/include"
)
target_compile_definitions(${ENGINE_NAME} PUBLIC
GLM_FORCE_RADIANS
GLM_FORCE_DEPTH_ZERO_TO_ONE
)
target_link_libraries(${ENGINE_NAME} PUBLIC
vulkan
shaderc
+12 -7
View File
@@ -3,21 +3,21 @@
#include <cstdlib>
#include <iostream>
#include <memory>
#include <sophia/asset_registry.hpp>
#include <sophia/camera_2d.hpp>
#include <sophia/canvas.hpp>
#include <sophia/console/console_command_registry.hpp>
#include <sophia/frame_info.hpp>
#include <sophia/math.hpp>
#include <sophia/resource_registry_ref.hpp>
#include <sophia/scene_registry.hpp>
#include <sophia/scene_view_registry.hpp>
#include <sophia/types.hpp>
#include <sophia/ui/debug_ui.hpp>
#include <sophia/ui/panel_registry.hpp>
#include <sophia/ui/ui.hpp>
#include <string>
#include <vector>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
namespace sophia {
class Application {
@@ -44,8 +44,11 @@ class Application {
virtual void onRender() {}
virtual void onDestroy() {}
AssetRegistry& assets() { return this->assetRegistry; }
ResourceRegistryRef resources();
PanelRegistry& panels() { return this->panelRegistry; }
SceneRegistry& scenes() { return this->sceneRegistry; }
SceneViewRegistry& views() { return this->sceneViewRegistry; }
UI& ui() { return this->_ui; }
DebugUI& debugUI() { return this->_debugUI; }
@@ -66,8 +69,10 @@ class Application {
class Impl;
std::unique_ptr<Impl> impl;
AssetRegistry assetRegistry;
PanelRegistry panelRegistry;
SceneRegistry sceneRegistry;
SceneViewRegistry sceneViewRegistry;
UI _ui;
DebugUI _debugUI;
};
-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
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <sophia/math.hpp>
namespace sophia {
+1 -4
View File
@@ -2,12 +2,9 @@
#include <memory>
#include <sophia/frame_info.hpp>
#include <sophia/math.hpp>
#include <vector>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
namespace sophia {
class Application;
+52 -7
View File
@@ -1,18 +1,17 @@
#pragma once
#include <sophia/asset_registry.hpp>
#include <string>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <iostream>
#include <ostream>
#include <sophia/handles.hpp>
#include <sophia/math.hpp>
#include <string>
namespace sophia {
struct TagComponent {
std::string tag; // entt::hashed_string ?
uint32_t mask = 0;
};
struct TransformComponent {
@@ -28,10 +27,56 @@ struct TransformComponent {
glm::mat3 normalMatrix() const {
return glm::mat3{glm::transpose(glm::inverse(mat4()))};
}
friend std::ostream& operator<<(std::ostream& os,
const TransformComponent& transform) {
os << "TransformComponent {\n"
<< " Position: (" << transform.position.x << ", "
<< transform.position.y << ", " << transform.position.z << ")\n"
<< " Rotation (w, x, y, z): (" << transform.rotation.w << ", "
<< transform.rotation.x << ", " << transform.rotation.y << ", "
<< transform.rotation.z << ")\n"
<< " Scale: (" << transform.scale.x << ", " << transform.scale.y << ", "
<< transform.scale.z << ")\n"
<< "}";
return os;
}
};
struct MeshComponent {
MeshHandle handle;
};
struct CameraComponent {
struct Orthographic {
float height = 10.f;
float nearPlane = -1.f;
float farPlane = 1.f;
};
struct Perspective {
float fov = glm::radians(60.f);
float nearPlane = 0.1f;
float farPlane = 100.f;
};
enum class Projection { Orthographic, Perspective };
Projection projection = Projection::Orthographic;
Orthographic ortho;
Perspective perspective;
glm::mat4 getProjection(float aspect) const {
if (projection == Projection::Perspective) {
return glm::perspective(perspective.fov, aspect, perspective.nearPlane,
perspective.farPlane);
}
float halfHeight = ortho.height / 2.f;
float halfWidth = halfHeight * aspect;
return glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight,
ortho.nearPlane, ortho.farPlane);
}
};
} // namespace sophia
+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
#include <sophia/camera_2d.hpp>
#include <sophia/math.hpp>
#include <sophia/types.hpp>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
namespace sophia {
struct FrameInfo {
+20 -5
View File
@@ -6,7 +6,7 @@
namespace sophia {
template <typename Tag>
template <typename T>
struct Handle {
static constexpr u32 INVALID_INDEX = (1u << 20) - 1;
@@ -24,10 +24,25 @@ struct Handle {
[[nodiscard]] auto operator<=>(const Handle&) const = default;
};
struct MeshTag {};
struct PanelTag {};
struct InvalidHandle {
template <typename T>
[[nodiscard]] constexpr operator Handle<T>() const {
return Handle<T>::invalid();
}
};
using MeshHandle = Handle<MeshTag>;
using PanelHandle = Handle<PanelTag>;
inline constexpr InvalidHandle INVALID_HANDLE{};
class Mesh;
class Panel;
class RenderTarget;
class Scene;
struct SceneView;
using MeshHandle = Handle<Mesh>;
using PanelHandle = Handle<Panel>;
using RenderTargetHandle = Handle<RenderTarget>;
using SceneHandle = Handle<Scene>;
using SceneViewHandle = Handle<SceneView>;
} // namespace sophia
+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 hide();
void showPanelRegistry(bool show = true);
void showFrameStats(bool show = true);
void showConsole(bool show = true);
void showPanelRegistry(bool show = false);
void showFrameStats(bool show = false);
void showConsole(bool show = false);
private:
PanelRegistry& registry;
+21 -35
View File
@@ -6,6 +6,7 @@
#include <sophia/ui/dock_layout.hpp>
#include <sophia/ui/panel.hpp>
#include <sophia/util/assert.hpp>
#include <sophia/util/pool.hpp>
#include <string_view>
#include <typeinfo>
#include <utility>
@@ -21,17 +22,17 @@ class PanelRegistry {
PanelRegistry() = default;
~PanelRegistry() = default;
PanelHandle add(std::unique_ptr<Panel> panel);
PanelHandle add(std::unique_ptr<Panel> panel) {
return pool.add(std::move(panel));
}
template <typename T, typename... Args>
PanelHandle emplace(Args&&... args) {
return add(std::make_unique<T>(std::forward<Args>(args)...));
}
const Panel& get(PanelHandle handle) const;
Panel& get(PanelHandle handle) {
return const_cast<Panel&>(std::as_const(*this).get(handle));
}
const Panel& get(PanelHandle handle) const { return pool.get(handle); }
Panel& get(PanelHandle handle) { return pool.get(handle); }
template <typename T>
const T& get(PanelHandle handle) const {
@@ -46,19 +47,17 @@ class PanelRegistry {
return const_cast<T&>(std::as_const(*this).template get<T>(handle));
}
void remove(PanelHandle handle);
bool isValid(PanelHandle handle) const;
void remove(PanelHandle handle) { pool.release(handle); }
bool isValid(PanelHandle handle) const { return pool.isAlive(handle); }
template <typename T>
const T* find() const {
for (const Slot& slot : slots) {
if (slot.panel) {
if (const T* panel = dynamic_cast<const T*>(slot.panel.get())) {
return panel;
}
}
}
return nullptr;
const T* result = nullptr;
pool.findIf([&result](const Panel& panel) {
result = dynamic_cast<const T*>(&panel);
return result != nullptr;
});
return result;
}
template <typename T>
@@ -67,29 +66,22 @@ class PanelRegistry {
}
const Panel* find(std::string_view name) const {
for (const Slot& slot : slots) {
if (slot.panel && slot.panel->getName() == name) {
return slot.panel.get();
}
}
return nullptr;
return pool.findIf(
[name](const Panel& panel) { return panel.getName() == name; });
}
Panel* find(std::string_view name) {
return const_cast<Panel*>(std::as_const(*this).find(name));
}
u32 size() const;
u32 size() const { return pool.size(); }
u32 visibleCount() const;
std::vector<std::string_view> names() const {
std::vector<std::string_view> result;
result.reserve(slots.size());
for (const Slot& slot : slots) {
if (slot.panel) {
result.push_back(slot.panel->getName());
}
}
result.reserve(pool.size());
pool.forEach(
[&result](const Panel& panel) { result.push_back(panel.getName()); });
return result;
}
@@ -101,13 +93,7 @@ class PanelRegistry {
const DockLayout& getDefaultLayout() const { return defaultLayout; }
private:
struct Slot {
std::unique_ptr<Panel> panel;
u32 generation = 0;
};
std::vector<Slot> slots;
std::vector<u32> freeList;
Pool<Panel> pool{"Panel"};
DockLayout defaultLayout;
};
+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 <chrono>
#include <cstddef>
#include <glm/glm.hpp>
#include <map>
#include <sophia/application.hpp>
#include <sophia/util/logger.hpp>
#include <string>
@@ -8,20 +13,18 @@
#include <vector>
#include <vulkan/vulkan.hpp>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <imgui.h>
#include <glm/glm.hpp>
#include "buffer.hpp"
#include "device.hpp"
#include "imgui_renderer.hpp"
#include "mesh.hpp"
#include "pipelines/graphics_pipeline.hpp"
#include "render_context.hpp"
#include "shader.hpp"
#include "systems/model_render_system.hpp"
#include "rendering/imgui_renderer.hpp"
#include "rendering/model_render_system.hpp"
#include "rendering/render_context.hpp"
#include "rendering/render_graph.hpp"
#include "rendering/scene_renderer.hpp"
#include "resources/mesh.hpp"
#include "resources/render_target.hpp"
#include "resources/resource_registry.hpp"
#include "vulkan/buffer.hpp"
#include "vulkan/device.hpp"
#include "vulkan/pipelines/graphics_pipeline.hpp"
#include "vulkan/shader.hpp"
#include "window.hpp"
namespace sophia {
@@ -41,6 +44,12 @@ struct PushConstants {
std::vector<std::string> commandLineArgs;
constexpr ClearColor CLEAR_COLOR = {0.01f, 0.01f, 0.02f, 1.0f};
bool hasFlag(ClearFlags flags, ClearFlags flag) {
return (static_cast<u8>(flags) & static_cast<u8>(flag)) != 0;
}
} // namespace
class Application::Impl {
@@ -50,12 +59,17 @@ class Application::Impl {
config{config},
window{config.width, config.height, config.name},
device{&window},
renderContext{window, device} {}
renderContext{window, device},
resourceRegistry{device, renderContext.getImageFormat()} {}
void run() {
ModelRenderSystem modelRenderSystem{device, renderContext};
SceneRenderer sceneRenderer{this->application.sceneRegistry,
this->resourceRegistry};
ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(),
renderContext.getImageCount()};
renderContext.getImageCount(),
this->resourceRegistry};
this->application._ui.renderer = &imguiRenderer;
this->application.consoleCommandRegistry.registerCommand(
"reset_layout", "Restore the default panel layout",
@@ -65,8 +79,17 @@ class Application::Impl {
});
this->application.onCreate();
imguiRenderer.setDefaultLayout(
this->application.panelRegistry.getDefaultLayout());
auto& meshPool = this->resourceRegistry.meshPool;
auto& renderTargetPool = this->resourceRegistry.renderTargetPool;
PanelRegistry& panelRegistry = this->application.panelRegistry;
SceneRegistry& sceneRegistry = this->application.sceneRegistry;
SceneViewRegistry& sceneViewRegistry = this->application.sceneViewRegistry;
const SceneViewRegistry& sceneViewRegistryConst =
this->application.sceneViewRegistry;
imguiRenderer.setDefaultLayout(panelRegistry.getDefaultLayout());
auto startTime = std::chrono::high_resolution_clock::now();
auto currentTime = startTime;
@@ -83,6 +106,10 @@ class Application::Impl {
double elapsedTime =
std::chrono::duration<double>(currentTime - startTime).count();
// Resizing waits for the GPU, so it has to happen before any of this
// frame's work (or ImGui's references to the targets) is recorded.
renderTargetPool.applyPendingResizes();
vk::Extent2D extent = renderContext.getExtent();
FrameInfo frameInfo{frameIndex, elapsedTime, deltaTime,
glm::vec2(static_cast<float>(extent.width),
@@ -102,41 +129,9 @@ class Application::Impl {
imguiRenderer.beginFrame();
this->application.panelRegistry.update(frameInfo);
panelRegistry.update(frameInfo);
this->application.onRender();
renderContext.transitionCurrentImage(
commandBuffer, vk::ImageLayout::eColorAttachmentOptimal);
vk::ClearValue clearValue{};
clearValue.color =
vk::ClearColorValue{std::array<float, 4>{0.01f, 0.01f, 0.02f, 1.0f}};
vk::RenderingAttachmentInfo colorAttachment{};
colorAttachment.imageView = renderContext.getCurrentImageView();
colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal;
colorAttachment.loadOp = vk::AttachmentLoadOp::eClear;
colorAttachment.storeOp = vk::AttachmentStoreOp::eStore;
colorAttachment.clearValue = clearValue;
vk::RenderingInfo renderingInfo{};
renderingInfo.renderArea = vk::Rect2D{{0, 0}, extent};
renderingInfo.layerCount = 1;
renderingInfo.colorAttachmentCount = 1;
renderingInfo.pColorAttachments = &colorAttachment;
commandBuffer.beginRendering(renderingInfo);
vk::Viewport viewport{0.0f,
0.0f,
static_cast<float>(extent.width),
static_cast<float>(extent.height),
0.0f,
1.0f};
vk::Rect2D scissor{{0, 0}, extent};
commandBuffer.setViewport(0, viewport);
commandBuffer.setScissor(0, scissor);
std::vector<ModelRenderSystem::MeshInstance> instances;
instances.reserve(this->meshes.size() + this->drawCalls.size());
for (const std::unique_ptr<Mesh>& mesh : this->meshes) {
@@ -144,16 +139,61 @@ class Application::Impl {
}
instances.insert(instances.end(), this->drawCalls.begin(),
this->drawCalls.end());
modelRenderSystem.render(commandBuffer, instances, frameInfo);
this->drawCalls.clear();
RenderGraph graph;
ImageRef backbuffer =
graph.importImage(renderContext.getCurrentImage(), Access::Present);
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;
}
auto [it, inserted] = viewTargets.try_emplace(view.target);
if (inserted) {
it->second =
graph.importImage(renderTargetPool.get(view.target).getImage(),
Access::SampledRead);
}
const vec4& color = view.clearColor;
graph.addPass(
"scene view",
[&](RenderGraph::PassBuilder& pass) {
pass.writeColor(it->second,
hasFlag(view.clearFlags, ClearFlags::Color)
? vk::AttachmentLoadOp::eClear
: vk::AttachmentLoadOp::eLoad,
{color.r, color.g, color.b, color.a});
},
[&sceneRenderer, &sceneViewRegistry, handle](vk::CommandBuffer) {
const SceneView& view = sceneViewRegistry.get(handle);
if (view.camera) {
sceneRenderer.render(view);
}
});
}
graph.addPass(
"main",
[&](RenderGraph::PassBuilder& pass) {
pass.writeColor(backbuffer, vk::AttachmentLoadOp::eClear,
CLEAR_COLOR);
for (const auto& [handle, target] : viewTargets) {
pass.sample(target);
}
},
[&](vk::CommandBuffer commandBuffer) {
modelRenderSystem.render(commandBuffer, instances, frameInfo);
imguiRenderer.endFrame(commandBuffer);
});
commandBuffer.endRendering();
renderContext.transitionCurrentImage(commandBuffer,
vk::ImageLayout::ePresentSrcKHR);
graph.execute(commandBuffer);
renderContext.endFrame();
@@ -164,6 +204,8 @@ class Application::Impl {
device.waitIdle();
this->application._ui.renderer = nullptr;
auto endTime = std::chrono::high_resolution_clock::now();
double totalRuntime =
std::chrono::duration<double>(endTime - startTime).count();
@@ -193,7 +235,7 @@ class Application::Impl {
this->drawCalls.push_back({&mesh, transform});
}
Device& getDevice() { return device; }
ResourceRegistry& getResourceRegistry() { return resourceRegistry; }
private:
Application& application;
@@ -201,6 +243,7 @@ class Application::Impl {
Window window;
Device device;
RenderContext renderContext;
ResourceRegistry resourceRegistry;
std::vector<std::unique_ptr<Mesh>> meshes;
std::vector<ModelRenderSystem::MeshInstance> drawCalls;
@@ -214,13 +257,17 @@ Application::Application(const Config& config)
: impl{std::make_unique<Impl>(*this, config)},
canvas{this},
args{std::move(commandLineArgs)},
assetRegistry{impl->getDevice()},
_ui{ResourceRegistryRef{impl->getResourceRegistry()}},
_debugUI{this->panelRegistry, this->consoleCommandRegistry} {}
Application::~Application() = default;
void Application::run() { impl->run(); }
ResourceRegistryRef Application::resources() {
return ResourceRegistryRef{impl->getResourceRegistry()};
}
void Application::pushMesh(std::vector<CanvasVertex> vertices,
std::vector<u32> indices) {
impl->pushMesh(std::move(vertices), std::move(indices));
@@ -229,7 +276,7 @@ void Application::pushMesh(std::vector<CanvasVertex> vertices,
void Application::clearMeshes() { impl->clearMeshes(); }
void Application::drawMesh(MeshHandle handle, const glm::mat4& transform) {
impl->drawMesh(assetRegistry.get<Mesh>(handle), transform);
impl->drawMesh(impl->getResourceRegistry().meshPool.get(handle), transform);
}
} // namespace sophia
-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_impl_glfw.h>
#include <imgui_impl_vulkan.h>
#include <imgui_internal.h>
#include "resources/render_target.hpp"
namespace sophia {
namespace {
@@ -20,7 +22,8 @@ void dockAll(const std::vector<std::string>& windows, ImGuiID nodeId) {
ImGuiRenderer::ImGuiRenderer(Window& window, Device& device,
vk::Format colorAttachmentFormat,
u32 minImageCount) {
u32 minImageCount, ResourceRegistry& resources)
: resources{resources} {
// 1. Construct ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
@@ -67,12 +70,17 @@ ImGuiRenderer::ImGuiRenderer(Window& window, Device& device,
}
ImGuiRenderer::~ImGuiRenderer() {
for (const auto& [index, texture] : textures) {
ImGui_ImplVulkan_RemoveTexture(texture.descriptorSet);
}
ImGui_ImplVulkan_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
}
void ImGuiRenderer::beginFrame() {
releaseStaleTextures();
ImGui_ImplVulkan_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
@@ -82,6 +90,46 @@ void ImGuiRenderer::beginFrame() {
applyLayoutIfNeeded(dockspaceId, DOCKSPACE_FLAGS);
}
ImTextureID ImGuiRenderer::getTextureId(RenderTargetHandle handle) {
vk::ImageView view =
resources.renderTargetPool.get(handle).getImage().getView();
auto it = textures.find(handle.index);
if (it != textures.end()) {
if (it->second.generation == handle.generation && it->second.view == view) {
return (ImTextureID)it->second.descriptorSet;
}
ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet);
textures.erase(it);
}
// ImGui owns the sampler; it only needs the view and the layout the image is
// left in once the scene pass has finished.
VkDescriptorSet descriptorSet = ImGui_ImplVulkan_AddTexture(
static_cast<VkImageView>(view), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
textures[handle.index] = {handle.generation, view, descriptorSet};
return (ImTextureID)descriptorSet;
}
// Resizing and releasing a target both wait for the GPU before destroying its
// image, so nothing in flight can still be using a stale texture.
void ImGuiRenderer::releaseStaleTextures() {
for (auto it = textures.begin(); it != textures.end();) {
RenderTargetHandle handle{it->first, it->second.generation};
RenderTargetPool& renderTargetPool = resources.renderTargetPool;
bool stale =
!renderTargetPool.isAlive(handle) ||
renderTargetPool.get(handle).getImage().getView() != it->second.view;
if (stale) {
ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet);
it = textures.erase(it);
} else {
++it;
}
}
}
void ImGuiRenderer::setDefaultLayout(DockLayout layout) {
defaultLayout = std::move(layout);
seedIfUnsaved = true;
@@ -2,10 +2,13 @@
#include <imgui.h>
#include <sophia/handles.hpp>
#include <sophia/types.hpp>
#include <sophia/ui/dock_layout.hpp>
#include <unordered_map>
#include "device.hpp"
#include "resources/resource_registry.hpp"
#include "vulkan/device.hpp"
#include "window.hpp"
namespace sophia {
@@ -16,17 +19,32 @@ class ImGuiRenderer {
ImGuiRenderer& operator=(const ImGuiRenderer&) = delete;
ImGuiRenderer(Window& window, Device& device,
vk::Format colorAttachmentFormat, u32 minImageCount);
vk::Format colorAttachmentFormat, u32 minImageCount,
ResourceRegistry& resources);
~ImGuiRenderer();
void beginFrame();
void endFrame(vk::CommandBuffer commandBuffer);
ImTextureID getTextureId(RenderTargetHandle handle);
void setDefaultLayout(DockLayout layout);
void resetLayout();
private:
void applyLayoutIfNeeded(ImGuiID dockspaceId, ImGuiDockNodeFlags flags);
void releaseStaleTextures();
struct Texture {
u32 generation;
vk::ImageView view;
VkDescriptorSet descriptorSet;
};
ResourceRegistry& resources;
// Keyed by RenderTargetHandle::index.
std::unordered_map<u32, Texture> textures;
DockLayout defaultLayout;
bool seedIfUnsaved = false;
@@ -1,4 +1,4 @@
#include "model_render_system.hpp"
#include "rendering/model_render_system.hpp"
#include <sophia/util/assert.hpp>
@@ -4,10 +4,10 @@
#include <sophia/frame_info.hpp>
#include <span>
#include "device.hpp"
#include "mesh.hpp"
#include "pipelines/graphics_pipeline.hpp"
#include "render_context.hpp"
#include "rendering/render_context.hpp"
#include "resources/mesh.hpp"
#include "vulkan/device.hpp"
#include "vulkan/pipelines/graphics_pipeline.hpp"
namespace sophia {
@@ -1,10 +1,12 @@
#include "render_context.hpp"
#include "rendering/render_context.hpp"
#include <limits>
#include <sophia/util/assert.hpp>
#include <sophia/util/logger.hpp>
#include <stdexcept>
#include "vulkan/access.hpp"
namespace sophia {
RenderContext::RenderContext(Window& window, Device& device)
@@ -80,8 +82,7 @@ void RenderContext::endFrame() {
vk::Semaphore waitSemaphore = frame.imageAvailable.get();
vk::Semaphore signalSemaphore =
this->renderFinishedSemaphores.at(this->currentImageIndex).get();
vk::PipelineStageFlags waitStage =
vk::PipelineStageFlagBits::eColorAttachmentOutput;
vk::PipelineStageFlags waitStage = SWAPCHAIN_ACQUIRE_STAGE;
vk::CommandBuffer commandBuffer = frame.commandBuffer;
vk::SubmitInfo submitInfo{};
@@ -6,8 +6,8 @@
#include <vector>
#include <vulkan/vulkan.hpp>
#include "device.hpp"
#include "swapchain.hpp"
#include "vulkan/device.hpp"
#include "vulkan/swapchain.hpp"
#include "window.hpp"
namespace sophia {
@@ -31,14 +31,8 @@ class RenderContext {
vk::CommandBuffer beginFrame();
void endFrame();
void transitionCurrentImage(vk::CommandBuffer commandBuffer,
vk::ImageLayout newLayout) const {
this->swapchain->getImage(static_cast<int>(this->currentImageIndex))
.transitionLayout(commandBuffer, newLayout);
}
vk::ImageView getCurrentImageView() const {
return this->swapchain->getImageView(
static_cast<int>(this->currentImageIndex));
Image& getCurrentImage() const {
return this->swapchain->getImage(static_cast<int>(this->currentImageIndex));
}
vk::Extent2D getExtent() const { return this->swapchain->getExtent(); }
+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/logger.hpp>
@@ -1,23 +1,18 @@
#pragma once
#include <memory>
#include <sophia/asset_registry.hpp>
#include <sophia/handles.hpp>
#include <sophia/math.hpp>
#include <sophia/types.hpp>
#include <vector>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include "buffer.hpp"
#include "device.hpp"
#include "vulkan/buffer.hpp"
#include "vulkan/device.hpp"
namespace sophia {
class Mesh {
public:
using HandleType = MeshHandle;
struct Vertex {
glm::vec2 position{};
glm::vec3 color{};
@@ -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/scene.hpp>
#include <sophia/util/logger.hpp>
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->frameStatsHandle = registry.emplace<FrameStatsPanel>();
this->consoleHandle = registry.emplace<ConsolePanel>(commands);
hide();
// #endif // !NDEBUG
}
+7 -72
View File
@@ -1,88 +1,23 @@
#include <sophia/ui/panel_registry.hpp>
#include <sophia/util/assert.hpp>
namespace sophia {
namespace {
constexpr u32 GENERATION_MASK = (1u << 12) - 1;
} // namespace
PanelHandle PanelRegistry::add(std::unique_ptr<Panel> panel) {
if (!freeList.empty()) {
u32 index = freeList.back();
freeList.pop_back();
slots[index].panel = std::move(panel);
return PanelHandle{index, slots[index].generation};
}
slots.push_back(Slot{std::move(panel), 0});
return PanelHandle{static_cast<u32>(slots.size() - 1), 0};
}
const Panel& PanelRegistry::get(PanelHandle handle) const {
SOPHIA_ASSERT(handle.isValid(), "invalid PanelHandle");
SOPHIA_ASSERT(handle.index < slots.size(),
"PanelHandle index out of range:", handle.index);
const Slot& slot = slots[handle.index];
SOPHIA_ASSERT(slot.generation == handle.generation,
"stale PanelHandle (generation mismatch):", handle.index);
SOPHIA_ASSERT(slot.panel != nullptr,
"PanelHandle refers to a removed panel:", handle.index);
return *slot.panel;
}
void PanelRegistry::remove(PanelHandle handle) {
SOPHIA_ASSERT(handle.isValid(), "invalid PanelHandle");
SOPHIA_ASSERT(handle.index < slots.size(),
"PanelHandle index out of range:", handle.index);
Slot& slot = slots[handle.index];
SOPHIA_ASSERT(slot.generation == handle.generation,
"stale PanelHandle (generation mismatch):", handle.index);
SOPHIA_ASSERT(slot.panel != nullptr,
"PanelHandle already removed:", handle.index);
slot.panel.reset();
slot.generation = (slot.generation + 1) & GENERATION_MASK;
freeList.push_back(handle.index);
}
bool PanelRegistry::isValid(PanelHandle handle) const {
if (!handle.isValid() || handle.index >= slots.size()) {
return false;
}
const Slot& slot = slots[handle.index];
return slot.generation == handle.generation && slot.panel != nullptr;
}
u32 PanelRegistry::size() const {
u32 count = 0;
for (const Slot& slot : slots) {
if (slot.panel) {
count++;
}
}
return count;
}
u32 PanelRegistry::visibleCount() const {
u32 count = 0;
for (const Slot& slot : slots) {
if (slot.panel && slot.panel->isVisible()) {
pool.forEach([&count](const Panel& panel) {
if (panel.isVisible()) {
count++;
}
}
});
return count;
}
void PanelRegistry::update(const FrameInfo& frameInfo) {
for (auto& slot : slots) {
if (slot.panel && slot.panel->isVisible()) {
slot.panel->draw(frameInfo);
}
pool.forEach([&frameInfo](Panel& panel) {
if (panel.isVisible()) {
panel.draw(frameInfo);
}
});
}
} // namespace sophia
+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
* https://github.com/blurrypiano/littleVulkanEngine/blob/main/src/lve_buffer.cpp
*/
#include "buffer.hpp"
#include "vulkan/buffer.hpp"
#include <sophia/util/assert.hpp>
@@ -4,7 +4,7 @@
*/
#pragma once
#include "device.hpp"
#include "vulkan/device.hpp"
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 <vector>
#include "device.hpp"
#include "vulkan/device.hpp"
namespace sophia {
@@ -4,7 +4,7 @@
#include <unordered_map>
#include <vector>
#include "device.hpp"
#include "vulkan/device.hpp"
namespace sophia {
@@ -4,7 +4,7 @@
#include "descriptor_pool.hpp"
#include "descriptor_set_layout.hpp"
#include "device.hpp"
#include "vulkan/device.hpp"
namespace sophia {
@@ -1,10 +1,12 @@
#include "device.hpp"
#include "vulkan/device.hpp"
#include <string.h>
#include <set>
#include <sophia/util/logger.hpp>
#include "vulkan/debug_label.hpp"
namespace sophia {
static VKAPI_ATTR vk::Bool32 VKAPI_CALL
@@ -288,6 +290,8 @@ void Device::setupDebugMessenger() {
nullptr, &this->debugMessenger) != VK_SUCCESS) {
throw std::runtime_error("failed to set up debug callback!");
}
loadDebugLabelFunctions(*instance);
}
bool Device::checkValidationLayerSupport() {
@@ -1,81 +1,42 @@
#include "image.hpp"
#include "vulkan/image.hpp"
#include <cstring>
#include <sophia/util/logger.hpp>
#include <stdexcept>
#include "device.hpp"
#include "vulkan/device.hpp"
namespace sophia {
void Image::transitionLayout(vk::CommandBuffer commandBuffer,
vk::ImageLayout newLayout) {
vk::ImageMemoryBarrier barrier{};
barrier.oldLayout = this->layout;
barrier.newLayout = newLayout;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = this->get();
barrier.subresourceRange.aspectMask = this->aspectMask;
barrier.subresourceRange.baseMipLevel = 0;
barrier.subresourceRange.levelCount = this->mipLevels;
barrier.subresourceRange.baseArrayLayer = 0;
barrier.subresourceRange.layerCount = this->arrayLayers;
vk::PipelineStageFlags srcStage;
vk::PipelineStageFlags dstStage;
if (this->layout == vk::ImageLayout::eUndefined &&
newLayout == vk::ImageLayout::eTransferDstOptimal) {
barrier.srcAccessMask = {};
barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite;
srcStage = vk::PipelineStageFlagBits::eTopOfPipe;
dstStage = vk::PipelineStageFlagBits::eTransfer;
} else if (this->layout == vk::ImageLayout::eTransferDstOptimal &&
newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) {
barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
srcStage = vk::PipelineStageFlagBits::eTransfer;
dstStage = vk::PipelineStageFlagBits::eFragmentShader;
} else if (this->layout == vk::ImageLayout::eUndefined &&
newLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) {
barrier.srcAccessMask = {};
barrier.dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead |
vk::AccessFlagBits::eDepthStencilAttachmentWrite;
srcStage = vk::PipelineStageFlagBits::eTopOfPipe;
dstStage = vk::PipelineStageFlagBits::eEarlyFragmentTests;
} else if (this->layout == vk::ImageLayout::eUndefined &&
newLayout == vk::ImageLayout::eColorAttachmentOptimal) {
barrier.srcAccessMask = {};
barrier.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
srcStage = vk::PipelineStageFlagBits::eTopOfPipe;
dstStage = vk::PipelineStageFlagBits::eColorAttachmentOutput;
} else if (this->layout == vk::ImageLayout::eUndefined &&
newLayout == vk::ImageLayout::eGeneral) {
barrier.srcAccessMask = {};
barrier.dstAccessMask =
vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite;
srcStage = vk::PipelineStageFlagBits::eTopOfPipe;
dstStage = vk::PipelineStageFlagBits::eComputeShader;
} else if (this->layout == vk::ImageLayout::eColorAttachmentOptimal &&
newLayout == vk::ImageLayout::ePresentSrcKHR) {
barrier.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
barrier.dstAccessMask = {};
srcStage = vk::PipelineStageFlagBits::eColorAttachmentOutput;
dstStage = vk::PipelineStageFlagBits::eBottomOfPipe;
} else if (this->layout == vk::ImageLayout::ePresentSrcKHR &&
newLayout == vk::ImageLayout::eColorAttachmentOptimal) {
barrier.srcAccessMask = {};
barrier.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
srcStage = vk::PipelineStageFlagBits::eTopOfPipe;
dstStage = vk::PipelineStageFlagBits::eColorAttachmentOutput;
} else {
log::fatal("Unsupported image layout transition.");
throw std::runtime_error("Unsupported image layout transition.");
vk::ImageSubresourceRange Image::getSubresourceRange() const {
return vk::ImageSubresourceRange{this->aspectMask, 0, this->mipLevels, 0,
this->arrayLayers};
}
commandBuffer.pipelineBarrier(srcStage, dstStage, {}, nullptr, nullptr,
AccessState Image::getAccessState() const {
AccessState state = getStateForLayout(this->layout);
// A wrapped (swapchain) image is handed to us by the acquire semaphore, so
// whatever we do next has to be ordered after that wait.
if (this->isExternal() && (this->layout == vk::ImageLayout::eUndefined ||
this->layout == vk::ImageLayout::ePresentSrcKHR)) {
state.stage = SWAPCHAIN_ACQUIRE_STAGE;
}
return state;
}
void Image::transitionLayout(vk::CommandBuffer commandBuffer,
vk::ImageLayout newLayout) {
AccessState from = this->getAccessState();
AccessState to = getStateForLayout(newLayout);
if (needsBarrier(from, to)) {
vk::ImageMemoryBarrier barrier =
makeImageBarrier(this->get(), this->getSubresourceRange(), from, to);
commandBuffer.pipelineBarrier(from.stage, to.stage, {}, nullptr, nullptr,
barrier);
}
this->layout = newLayout;
}
@@ -4,6 +4,8 @@
#include <sophia/types.hpp>
#include <vulkan/vulkan.hpp>
#include "vulkan/access.hpp"
namespace sophia {
class Device;
@@ -28,6 +30,20 @@ class Image {
vk::Format getFormat() const { return this->format; }
vk::Extent3D getExtent() const { return this->extent; }
vk::ImageLayout getLayout() const { return this->layout; }
vk::ImageSubresourceRange getSubresourceRange() const;
/**
* The layout this image was last left in, plus the stages and accesses that
* a barrier out of it has to wait on.
*/
AccessState getAccessState() const;
/**
* Updates the tracked layout without recording a barrier. For code that
* does its own transitions (the RenderGraph) and needs to hand the final
* layout back.
*/
void recordLayout(vk::ImageLayout newLayout) { this->layout = newLayout; }
void transitionLayout(vk::CommandBuffer commandBuffer,
vk::ImageLayout newLayout);
@@ -3,9 +3,9 @@
#include <memory>
#include <vulkan/vulkan.hpp>
#include "device.hpp"
#include "pipeline.hpp"
#include "shader.hpp"
#include "vulkan/device.hpp"
#include "vulkan/shader.hpp"
namespace sophia {
@@ -6,9 +6,9 @@
#include <vector>
#include <vulkan/vulkan.hpp>
#include "device.hpp"
#include "pipeline.hpp"
#include "shader.hpp"
#include "vulkan/device.hpp"
#include "vulkan/shader.hpp"
namespace sophia {
@@ -4,7 +4,7 @@
#include <vector>
#include <vulkan/vulkan.hpp>
#include "device.hpp"
#include "vulkan/device.hpp"
namespace sophia {
@@ -1,4 +1,4 @@
#include "shader.hpp"
#include "vulkan/shader.hpp"
#include <fstream>
#include <shaderc/shaderc.hpp>
@@ -74,8 +74,7 @@ void Shader::load(const std::string& path, ShaderStage shaderStage) {
throw std::runtime_error("invalid SPIR-V binary: " + path);
}
this->spirv.assign(
reinterpret_cast<const u32*>(bytes.data()),
this->spirv.assign(reinterpret_cast<const u32*>(bytes.data()),
reinterpret_cast<const u32*>(bytes.data() + bytes.size()));
log::info("Loaded precompiled shader: " + path + " (" +
@@ -1,4 +1,4 @@
#include "swapchain.hpp"
#include "vulkan/swapchain.hpp"
#include <algorithm>
#include <limits>
@@ -5,8 +5,8 @@
#include <vector>
#include <vulkan/vulkan.hpp>
#include "device.hpp"
#include "image.hpp"
#include "vulkan/device.hpp"
#include "vulkan/image.hpp"
namespace sophia {
@@ -23,9 +23,6 @@ class Swapchain {
Image& getImage(int index) const { return *this->images.at(index); }
Image& getDepthImage(int index) const { return *this->depthImages.at(index); }
vk::ImageView getImageView(int index) const {
return this->images.at(index)->getView();
}
vk::ImageView getDepthImageView(int index) const {
return this->depthImages.at(index)->getView();
}
+4 -7
View File
@@ -1,6 +1,9 @@
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <glm/glm.hpp>
#include <glm/gtc/constants.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <iostream>
#include <random>
#include <sophia/application.hpp>
@@ -10,12 +13,6 @@
#include <string>
#include <vector>
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/constants.hpp>
#include <glm/gtc/matrix_transform.hpp>
struct Boid {
glm::vec2 position;
glm::vec2 velocity;
@@ -91,7 +88,7 @@ class Boids2D : public sophia::Application {
{{-BOID_LENGTH * 0.5f, BOID_WIDTH * 0.5f}, {1.0f, 1.0f, 1.0f}},
{{-BOID_LENGTH * 0.5f, -BOID_WIDTH * 0.5f}, {1.0f, 1.0f, 1.0f}},
};
boidMesh = assets().loadMesh(std::move(vertices), {});
boidMesh = resources().create<sophia::Mesh>(std::move(vertices));
}
void onUpdate(const sophia::FrameInfo& frameInfo) override {
+28 -4
View File
@@ -1,17 +1,41 @@
#include <iostream>
#include <imgui.h>
#include <sophia/application.hpp>
#include <sophia/entity.hpp>
#include <sophia/frame_info.hpp>
#include <sophia/scene_view.hpp>
#include <sophia/ui/panel.hpp>
class Viewport : public sophia::Panel {
public:
Viewport(sophia::UI& ui, sophia::RenderTargetHandle target)
: Panel{{.name = "Viewport"}}, ui{ui}, target{target} {}
void onUpdate(const sophia::FrameInfo&) override {
ImGui::Text("Test ViewPort");
ui.image(target);
}
private:
sophia::UI& ui;
sophia::RenderTargetHandle target;
};
class Testbed : public sophia::Application {
public:
Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {}
void onCreate() override {
std::cout << "Testbed created" << std::endl;
view = views().create({.scene = scenes().create(),
.target = resources().create<sophia::RenderTarget>(),
.clearColor = {0.1f, 0.1f, 0.15f, 1.f}});
debugUI().show();
panels().emplace<Viewport>(ui(), views().get(view).target);
}
void onUpdate(const sophia::FrameInfo& frameInfo) override {}
private:
sophia::SceneViewHandle view;
};
SOPHIA_MAIN(Testbed)