Merge pull request '2D scene rendering + image loading' (#15) from issue-14/2d-scene-support-spritecomponent-2d-render-system into main

Reviewed-on: #15
This commit was merged in pull request #15.
This commit is contained in:
2026-09-30 15:56:33 -07:00
76 changed files with 10268 additions and 711 deletions
+1 -2
View File
@@ -14,5 +14,4 @@ TODO
## Testing Instructions
1. Build engine via `bin/dev build && cd build/ && make -j4`
2. Run testbed via `./testbed`
1. Build engine and run editor via `bin/dev editor`
+7
View File
@@ -32,6 +32,13 @@ pids
*.pid.lock
imgui.ini
# Python bytecode (bin/dev and bin/dev_commands)
__pycache__/
# Tracy captures
traces/
*.tracy
# dotenv environment variables file
.envrc
.env
+3
View File
@@ -8,3 +8,6 @@
path = third_party/imgui
url = https://github.com/ocornut/imgui.git
branch = docking
[submodule "third_party/tracy"]
path = third_party/tracy
url = https://github.com/wolfpld/tracy.git
+2
View File
@@ -1,5 +1,7 @@
# sophia
![showcase](./media/showcase.png)
## Getting Started
COMING SOON
+23 -248
View File
@@ -2,13 +2,16 @@
import json
import os
import re
import shlex
import shutil
import struct
import subprocess
import sys
from helpers.gitea import GiteaCommand
from helpers.renderdoc import RenderDocCommand
from helpers.shaders import ShadersCommand
from helpers.tracy import TracyCommand
COMMAND_TO_METHOD = {
"branch-from": "branch_from",
"bash-completions": "bash_completions",
@@ -17,8 +20,7 @@ METHOD_TO_COMMAND = {method: command for command, method in COMMAND_TO_METHOD.it
WAIT_FOR_PROCESS = "wait_for_process"
class DevHelper:
SHADER_EXTENSIONS = (".vert", ".frag", ".comp", ".geom", ".tesc", ".tese")
class DevCommand(GiteaCommand, RenderDocCommand, ShadersCommand, TracyCommand):
PROJECT_NAME = "sophia"
DEFAULT_PRESET = "debug"
@@ -50,6 +52,20 @@ class DevHelper:
preset, fresh = self._parse_preset_args(args, "build")
self._build_preset(preset, fresh)
##
# Builds the debug preset (incrementally), then runs the editor from
# build/debug. Any arguments are passed through to the editor.
# dev editor [args...]
def editor(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", "editor")
print(f"Running: {shlex.join([binary, *args])}")
os.chdir(preset_dir)
os.execv(binary, [binary, *args])
##
# Builds the debug preset (incrementally), then runs the testbed from
# build/debug. Any arguments are passed through to the testbed.
@@ -64,87 +80,6 @@ class DevHelper:
os.chdir(preset_dir)
os.execv(binary, [binary, *args])
##
# Checks out a branch named after a Gitea issue URL.
# If the issue is from this project's repo, the branch name will be:
# issue-<issue_number>/<issue_title>
# If the issue is from another repo (presumably upstream), the branch name will be:
# <issue_owner>-issue-<issue_number>/<issue_title>
def branch_from(self, *args, **kwargs):
issue_url = args[0] if args else None
if issue_url is None:
raise ValueError("Must provide a Gitea issue URL.")
base_url, issue_repo, issue_number = self._parse_issue_url(issue_url)
issue_title = self._fetch_issue_title(base_url, issue_repo, issue_number)
branch_name = self._format_branch_name(issue_repo, issue_number, issue_title)
exists = subprocess.run(
["git", "-C", self._project_root(), "rev-parse", "--verify", "--quiet", f"refs/heads/{branch_name}"],
stdout=subprocess.DEVNULL,
).returncode == 0
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)
def shaders(self, *args, **kwargs):
shaders_dir = os.path.join(self._project_root(), "engine", "src", "shaders")
if not os.path.isdir(shaders_dir):
raise ValueError(f"Shaders directory not found: {shaders_dir}")
glslc = self._find_glslc()
shader_paths = sorted(
os.path.join(root, filename)
for root, _dirs, files in os.walk(shaders_dir)
for filename in files
if filename.endswith(self.SHADER_EXTENSIONS)
)
if not shader_paths:
print(f"No shaders found in {shaders_dir}")
return
for shader_path in shader_paths:
self._generate_shader_header(glslc, shader_path)
# Custom helpers
def ownit(self, *args, **kwargs):
@@ -166,8 +101,8 @@ class DevHelper:
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))
for name in dir(DevCommand)
if not name.startswith("_") and name != "call" and callable(getattr(DevCommand, name))
)
def _parse_preset_args(self, args, command):
@@ -216,166 +151,6 @@ class DevHelper:
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:
raise ValueError(f"Not a Gitea issue URL: {url}")
return match[1], f"{match[2]}/{match[3]}", match[4]
def _fetch_issue_title(self, base_url, repo, issue_number):
if shutil.which("tea") is None:
raise RuntimeError("tea not found on PATH (install gitea tea and run `tea login add`)")
endpoint = f"{base_url}/api/v1/repos/{repo}/issues/{issue_number}"
print(f"Running: tea api {endpoint}")
result = subprocess.run(["tea", "api", endpoint], stdout=subprocess.PIPE, text=True)
if result.returncode != 0:
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():
return f"issue-{issue_number}/{formatted_title}"
issue_owner = issue_repo.split("/")[0]
return f"{issue_owner}-issue-{issue_number}/{formatted_title}"
def _origin_repo(self):
result = subprocess.run(
["git", "-C", self._project_root(), "remote", "get-url", "origin"],
stdout=subprocess.PIPE,
text=True,
)
match = re.search(r"[:/]([^/:]+/[^/]+?)(?:\.git)?$", result.stdout.strip())
return match[1] if match else None
def _find_glslc(self):
glslc = shutil.which("glslc")
if glslc is None:
raise RuntimeError(
"glslc not found on PATH (install the Vulkan SDK)"
)
return glslc
def _find_qrenderdoc(self):
qrenderdoc = shutil.which("qrenderdoc")
if qrenderdoc is None:
raise RuntimeError(
"qrenderdoc not found on PATH (install RenderDoc)"
)
return qrenderdoc
def _generate_shader_header(self, glslc, shader_path):
relative_path = os.path.relpath(shader_path, self._project_root())
print(f"Compiling shader: {relative_path}")
result = subprocess.run(
[glslc, shader_path, "-o", "-"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if result.returncode != 0:
raise RuntimeError(
f"failed to compile {shader_path}: {result.stderr.decode()}"
)
spirv = result.stdout
if len(spirv) % 4 != 0:
raise RuntimeError(
f"invalid SPIR-V output for {shader_path} (size not a multiple of 4)"
)
word_count = len(spirv) // 4
words = struct.unpack(f"<{word_count}I", spirv)
filename = os.path.basename(shader_path)
variable_name = f"{filename.replace('.', '_')}_spv"
header_path = shader_path + ".spv.h"
body_lines = [
" " + ", ".join(f"0x{word:08x}" for word in words[i:i + 4]) + ","
for i in range(0, len(words), 4)
]
header = "\n".join([
"#pragma once",
"",
"#include <array>",
"#include <cstdint>",
"",
"namespace sophia::shaders {",
"",
f"inline constexpr std::array<uint32_t, {word_count}> {variable_name} = {{",
*body_lines,
"};",
"",
"} // namespace sophia::shaders",
"",
])
with open(header_path, "w") as f:
f.write(header)
print(f"Wrote {os.path.relpath(header_path, self._project_root())} "
f"({word_count * 4} bytes)")
def _wait_for_process_with_logging(self, command):
process = self._exec(command, capture=True)
for line in process.stdout:
@@ -419,4 +194,4 @@ class DevHelper:
if __name__ == "__main__":
DevHelper().call(*sys.argv[1:])
DevCommand().call(*sys.argv[1:])
+156
View File
@@ -0,0 +1,156 @@
import json
import re
import shlex
import shutil
import subprocess
import sys
# Mixed into DevCommand (bin/dev), which provides PROJECT_NAME, _project_root
# and _exec_in_root.
class GiteaCommand:
##
# Checks out a branch named after a Gitea issue URL.
# If the issue is from this project's repo, the branch name will be:
# issue-<issue_number>/<issue_title>
# If the issue is from another repo (presumably upstream), the branch name will be:
# <issue_owner>-issue-<issue_number>/<issue_title>
def branch_from(self, *args, **kwargs):
issue_url = args[0] if args else None
if issue_url is None:
raise ValueError("Must provide a Gitea issue URL.")
base_url, issue_repo, issue_number = self._parse_issue_url(issue_url)
issue_title = self._fetch_issue_title(base_url, issue_repo, issue_number)
branch_name = self._format_branch_name(issue_repo, issue_number, issue_title)
exists = subprocess.run(
["git", "-C", self._project_root(), "rev-parse", "--verify", "--quiet", f"refs/heads/{branch_name}"],
stdout=subprocess.DEVNULL,
).returncode == 0
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 _parse_issue_url(self, url):
match = re.match(r"^(https?://[^/]+)/([^/]+)/([^/]+)/issues/(\d+)", url)
if match is None:
raise ValueError(f"Not a Gitea issue URL: {url}")
return match[1], f"{match[2]}/{match[3]}", match[4]
def _fetch_issue_title(self, base_url, repo, issue_number):
if shutil.which("tea") is None:
raise RuntimeError("tea not found on PATH (install gitea tea and run `tea login add`)")
endpoint = f"{base_url}/api/v1/repos/{repo}/issues/{issue_number}"
print(f"Running: tea api {endpoint}")
result = subprocess.run(["tea", "api", endpoint], stdout=subprocess.PIPE, text=True)
if result.returncode != 0:
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():
return f"issue-{issue_number}/{formatted_title}"
issue_owner = issue_repo.split("/")[0]
return f"{issue_owner}-issue-{issue_number}/{formatted_title}"
def _origin_repo(self):
result = subprocess.run(
["git", "-C", self._project_root(), "remote", "get-url", "origin"],
stdout=subprocess.PIPE,
text=True,
)
match = re.search(r"[:/]([^/:]+/[^/]+?)(?:\.git)?$", result.stdout.strip())
return match[1] if match else None
+16
View File
@@ -0,0 +1,16 @@
import shutil
# Mixed into DevCommand (bin/dev), which provides _exec_in_root.
class RenderDocCommand:
def renderdoc(self, *args, **kwargs):
qrenderdoc = self._find_qrenderdoc()
self._exec_in_root(qrenderdoc, replace=True)
def _find_qrenderdoc(self):
qrenderdoc = shutil.which("qrenderdoc")
if qrenderdoc is None:
raise RuntimeError(
"qrenderdoc not found on PATH (install RenderDoc)"
)
return qrenderdoc
+92
View File
@@ -0,0 +1,92 @@
import os
import shutil
import struct
import subprocess
# Mixed into DevCommand (bin/dev), which provides _project_root.
class ShadersCommand:
SHADER_EXTENSIONS = (".vert", ".frag", ".comp", ".geom", ".tesc", ".tese")
def shaders(self, *args, **kwargs):
shaders_dir = os.path.join(self._project_root(), "engine", "src", "shaders")
if not os.path.isdir(shaders_dir):
raise ValueError(f"Shaders directory not found: {shaders_dir}")
glslc = self._find_glslc()
shader_paths = sorted(
os.path.join(root, filename)
for root, _dirs, files in os.walk(shaders_dir)
for filename in files
if filename.endswith(self.SHADER_EXTENSIONS)
)
if not shader_paths:
print(f"No shaders found in {shaders_dir}")
return
for shader_path in shader_paths:
self._generate_shader_header(glslc, shader_path)
def _find_glslc(self):
glslc = shutil.which("glslc")
if glslc is None:
raise RuntimeError(
"glslc not found on PATH (install the Vulkan SDK)"
)
return glslc
def _generate_shader_header(self, glslc, shader_path):
relative_path = os.path.relpath(shader_path, self._project_root())
print(f"Compiling shader: {relative_path}")
result = subprocess.run(
[glslc, shader_path, "-o", "-"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if result.returncode != 0:
raise RuntimeError(
f"failed to compile {shader_path}: {result.stderr.decode()}"
)
spirv = result.stdout
if len(spirv) % 4 != 0:
raise RuntimeError(
f"invalid SPIR-V output for {shader_path} (size not a multiple of 4)"
)
word_count = len(spirv) // 4
words = struct.unpack(f"<{word_count}I", spirv)
filename = os.path.basename(shader_path)
variable_name = f"{filename.replace('.', '_')}_spv"
header_path = shader_path + ".spv.h"
body_lines = [
" " + ", ".join(f"0x{word:08x}" for word in words[i:i + 4]) + ","
for i in range(0, len(words), 4)
]
header = "\n".join([
"#pragma once",
"",
"#include <array>",
"#include <cstdint>",
"",
"namespace sophia::shaders {",
"",
f"inline constexpr std::array<uint32_t, {word_count}> {variable_name} = {{",
*body_lines,
"};",
"",
"} // namespace sophia::shaders",
"",
])
with open(header_path, "w") as f:
f.write(header)
print(f"Wrote {os.path.relpath(header_path, self._project_root())} "
f"({word_count * 4} bytes)")
+85
View File
@@ -0,0 +1,85 @@
import os
import re
import shlex
import time
# Mixed into DevCommand (bin/dev), which provides _project_root and
# _run_in_root.
class TracyCommand:
##
# Builds a Tracy tool from the third_party/tracy submodule, then runs it.
# Building from the submodule keeps the tool's version in sync with the
# client compiled into the engine.
# dev tracy [--fresh] [args...] Opens the profiler (viewer).
# dev tracy --save [name] [--fresh] [args...]
# Captures a trace headlessly to
# traces/<name>.tracy (default
# name: a timestamp) until the app
# exits or Ctrl+C.
# The profiler uses the X11 backend unless WAYLAND_DISPLAY is set. Tools
# are configured on first use (or --fresh, or when the backend changes),
# then built incrementally. Other arguments are passed through to the tool
# (e.g. a .tracy file to open, or `-s 10` to capture for 10 seconds).
def tracy(self, *args, **kwargs):
fresh, save, name, args = self._parse_tracy_args(args)
if save:
binary = self._build_tracy_tool("capture", "tracy-capture", {}, fresh)
traces_dir = os.path.join(self._project_root(), "traces")
os.makedirs(traces_dir, exist_ok=True)
name = name or time.strftime("%Y-%m-%d-%H%M%S")
if not name.endswith(".tracy"):
name += ".tracy"
args = ["-o", os.path.join(traces_dir, name), *args]
else:
legacy = "OFF" if os.environ.get("WAYLAND_DISPLAY") else "ON"
binary = self._build_tracy_tool("profiler", "tracy-profiler", {"LEGACY": legacy}, fresh)
print(f"Running: {shlex.join([binary, *args])}")
os.execv(binary, [binary, *args])
def _parse_tracy_args(self, args):
fresh = False
save = False
name = None
rest = []
args = list(args)
while args:
arg = args.pop(0)
if arg == "--fresh":
fresh = True
elif arg == "--save":
save = True
if args and not args[0].startswith("-"):
name = args.pop(0)
else:
rest.append(arg)
return fresh, save, name, rest
# Configures third_party/tracy/<tool> into <tool>/build when it has no
# build yet, --fresh is given, or a cached define differs from `defines`.
def _build_tracy_tool(self, tool, binary_name, defines, fresh):
source_dir = os.path.join("third_party", "tracy", tool)
build_dir = os.path.join(source_dir, "build")
cache_path = os.path.join(self._project_root(), build_dir, "CMakeCache.txt")
stale = not os.path.isfile(cache_path)
if not stale:
with open(cache_path) as f:
cache = f.read()
stale = any(
re.search(rf"^{key}:BOOL={value}$", cache, re.MULTILINE) is None
for key, value in defines.items()
)
if fresh or stale:
configure = ["cmake", "-B", build_dir, "-S", source_dir, "-DCMAKE_BUILD_TYPE=Release"]
configure += [f"-D{key}={value}" for key, value in defines.items()]
if fresh:
configure.append("--fresh")
self._run_in_root(configure)
self._run_in_root(["cmake", "--build", build_dir, "--parallel"])
return os.path.join(self._project_root(), build_dir, binary_name)
+54 -4
View File
@@ -1,5 +1,7 @@
#include <iostream>
#include <sophia/application.hpp>
#include <sophia/components.hpp>
#include <sophia/entity.hpp>
#include "panels/asset_browser_panel.hpp"
#include "panels/inspector_panel.hpp"
@@ -15,13 +17,19 @@ class Editor : public sophia::Application {
debugUI().show();
panels().emplace<SceneHierarchyPanel>();
panels().emplace<InspectorPanel>();
textures.emplace_back("Checker", createCheckerTexture());
sophia::SceneHandle sceneHandle = scenes().create();
sophia::Scene& scene = scenes().get(sceneHandle);
createDemoScene(scene);
panels().emplace<SceneHierarchyPanel>(scene, selected);
panels().emplace<InspectorPanel>(selected, textures);
panels().emplace<AssetBrowserPanel>();
sophia::RenderTargetHandle sceneTarget =
resources().create<sophia::RenderTarget>();
views().create({.scene = scenes().create(), .target = sceneTarget});
panels().emplace<ViewportPanel>(ui(), sceneTarget);
sceneViews().create({.scene = sceneHandle, .target = sceneTarget});
panels().emplace<ViewportPanel>(ui(), sceneTarget, scene, selected);
panels().setDefaultLayout(
{.left = {"Scene Hierarchy"},
@@ -31,6 +39,48 @@ class Editor : public sophia::Application {
}
void onUpdate(const sophia::FrameInfo& frameInfo) override {}
private:
void createDemoScene(sophia::Scene& scene) {
scene.createCamera("Main Camera");
sophia::Entity checker = scene.createEntity("Checker");
checker.getComponent<sophia::TransformComponent>().position = {-1.5f, 0.f,
0.f};
checker.addComponent<sophia::SpriteComponent>(sophia::SpriteComponent{
.texture = textures[0].second, .size = {2.f, 2.f}});
sophia::Entity red = scene.createEntity("Red");
red.getComponent<sophia::TransformComponent>().position = {1.f, -0.5f, 0.f};
red.addComponent<sophia::SpriteComponent>(sophia::SpriteComponent{
.color = {0.9f, 0.25f, 0.25f, 1.f}, .size = {2.f, 2.f}});
// Drawn over "Red" because of its higher sort order.
sophia::Entity blue = scene.createEntity("Blue (translucent)");
blue.getComponent<sophia::TransformComponent>().position = {1.75f, 0.25f,
0.f};
blue.addComponent<sophia::SpriteComponent>(
sophia::SpriteComponent{.color = {0.25f, 0.45f, 0.95f, 0.6f},
.size = {2.f, 2.f},
.sortOrder = 1});
}
sophia::TextureHandle createCheckerTexture() {
constexpr sophia::u32 SIZE = 8;
std::vector<sophia::u8> pixels(SIZE * SIZE * 4);
for (sophia::u32 y = 0; y < SIZE; y++) {
for (sophia::u32 x = 0; x < SIZE; x++) {
sophia::u8 value = ((x + y) % 2 == 0) ? 230 : 60;
sophia::u8* pixel = &pixels[(y * SIZE + x) * 4];
pixel[0] = pixel[1] = pixel[2] = value;
pixel[3] = 255;
}
}
return resources().create<sophia::Texture>(SIZE, SIZE, std::move(pixels));
}
sophia::Entity selected;
InspectorPanel::TextureList textures;
};
SOPHIA_MAIN(Editor)
+123 -18
View File
@@ -3,30 +3,135 @@
#include <imgui.h>
#include <cstdio>
#include <sophia/components.hpp>
void InspectorPanel::onUpdate(const sophia::FrameInfo&) {
char nameBuf[128];
std::snprintf(nameBuf, sizeof(nameBuf), "%s", name.c_str());
if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) {
name = nameBuf;
if (!selected) {
ImGui::TextDisabled("No entity selected");
return;
}
ImGui::Text("Tag: %s", tag.c_str());
drawTag();
ImGui::Separator();
drawTransform();
drawCamera();
drawSprite();
ImGui::Separator();
drawAddComponent();
}
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::DragFloat3("Position", &position.x, 0.1f);
ImGui::DragFloat3("Rotation", &rotation.x, 0.1f);
ImGui::DragFloat3("Scale", &scale.x, 0.1f);
}
void InspectorPanel::drawTag() {
auto& tag = selected.getComponent<sophia::TagComponent>();
if (ImGui::CollapsingHeader("Mesh Renderer",
ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Text("Mesh: %s", meshName.c_str());
ImGui::Text("Material: %s", materialName.c_str());
}
if (ImGui::CollapsingHeader("Script", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Text("%s", scriptName.c_str());
char nameBuf[128];
std::snprintf(nameBuf, sizeof(nameBuf), "%s", tag.tag.c_str());
if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) {
tag.tag = nameBuf;
}
}
void InspectorPanel::drawTransform() {
if (!selected.hasComponent<sophia::TransformComponent>() ||
!ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) {
return;
}
auto& transform = selected.getComponent<sophia::TransformComponent>();
ImGui::DragFloat3("Position", &transform.position.x, 0.1f);
// 2D only: edits rotation around Z and drops any X/Y rotation.
float degrees = glm::degrees(glm::eulerAngles(transform.rotation).z);
if (ImGui::DragFloat("Rotation", &degrees, 1.0f)) {
transform.rotation =
glm::angleAxis(glm::radians(degrees), glm::vec3{0.f, 0.f, 1.f});
}
ImGui::DragFloat3("Scale", &transform.scale.x, 0.1f);
}
void InspectorPanel::drawCamera() {
if (!selected.hasComponent<sophia::CameraComponent>() ||
!ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen)) {
return;
}
auto& camera = selected.getComponent<sophia::CameraComponent>();
int projection = static_cast<int>(camera.projection);
if (ImGui::Combo("Projection", &projection, "Orthographic\0Perspective\0")) {
camera.projection =
static_cast<sophia::CameraComponent::Projection>(projection);
}
if (camera.projection == sophia::CameraComponent::Projection::Orthographic) {
ImGui::DragFloat("Height", &camera.ortho.height, 0.1f, 0.01f, 10000.f);
} else {
float fov = glm::degrees(camera.perspective.fov);
if (ImGui::DragFloat("FOV", &fov, 0.5f, 1.f, 179.f)) {
camera.perspective.fov = glm::radians(fov);
}
}
}
void InspectorPanel::drawSprite() {
if (!selected.hasComponent<sophia::SpriteComponent>()) {
return;
}
bool open = true;
if (!ImGui::CollapsingHeader("Sprite", &open,
ImGuiTreeNodeFlags_DefaultOpen)) {
if (!open) {
selected.removeComponent<sophia::SpriteComponent>();
}
return;
}
auto& sprite = selected.getComponent<sophia::SpriteComponent>();
const char* current = "None";
for (const auto& [name, handle] : textures) {
if (handle == sprite.texture) {
current = name.c_str();
}
}
if (ImGui::BeginCombo("Texture", current)) {
if (ImGui::Selectable("None", !sprite.texture.isValid())) {
sprite.texture = sophia::INVALID_HANDLE;
}
for (const auto& [name, handle] : textures) {
if (ImGui::Selectable(name.c_str(), handle == sprite.texture)) {
sprite.texture = handle;
}
}
ImGui::EndCombo();
}
ImGui::ColorEdit4("Color", &sprite.color.x);
ImGui::DragFloat2("Size", &sprite.size.x, 0.05f);
ImGui::SliderFloat2("Pivot", &sprite.pivot.x, 0.f, 1.f);
ImGui::DragFloat4("UV Rect", &sprite.uvRect.x, 0.01f);
ImGui::DragInt("Sort Order", &sprite.sortOrder);
if (!open) {
selected.removeComponent<sophia::SpriteComponent>();
}
}
void InspectorPanel::drawAddComponent() {
if (ImGui::Button("Add Component")) {
ImGui::OpenPopup("AddComponent");
}
if (ImGui::BeginPopup("AddComponent")) {
if (!selected.hasComponent<sophia::SpriteComponent>() &&
ImGui::MenuItem("Sprite")) {
selected.addComponent<sophia::SpriteComponent>();
}
if (!selected.hasComponent<sophia::CameraComponent>() &&
ImGui::MenuItem("Camera")) {
selected.addComponent<sophia::CameraComponent>();
}
ImGui::EndPopup();
}
}
+17 -11
View File
@@ -1,25 +1,31 @@
#pragma once
#include <glm/glm.hpp>
#include <sophia/entity.hpp>
#include <sophia/frame_info.hpp>
#include <sophia/handles.hpp>
#include <sophia/ui/panel.hpp>
#include <string>
#include <utility>
#include <vector>
class InspectorPanel : public sophia::Panel {
public:
InspectorPanel() : Panel{{.name = "Inspector"}} {}
// Stand-in for an asset browser: the textures a sprite can pick from.
using TextureList =
std::vector<std::pair<std::string, sophia::TextureHandle>>;
InspectorPanel(sophia::Entity& selected, const TextureList& textures)
: Panel{{.name = "Inspector"}}, selected{selected}, textures{textures} {}
void onUpdate(const sophia::FrameInfo& frameInfo) override;
private:
std::string name = "Player";
std::string tag = "Player";
void drawTag();
void drawTransform();
void drawCamera();
void drawSprite();
void drawAddComponent();
glm::vec3 position{0.0f, 1.0f, 0.0f};
glm::vec3 rotation{0.0f, 0.0f, 0.0f};
glm::vec3 scale{1.0f, 1.0f, 1.0f};
std::string meshName = "player_capsule.fbx";
std::string materialName = "player_mat";
std::string scriptName = "PlayerController.lua";
sophia::Entity& selected;
const TextureList& textures;
};
+20 -20
View File
@@ -2,31 +2,31 @@
#include <imgui.h>
void SceneHierarchyPanel::drawNode(const Node& node) {
if (node.children.empty()) {
#include <sophia/components.hpp>
void SceneHierarchyPanel::onUpdate(const sophia::FrameInfo&) {
if (ImGui::Button("+ Sprite")) {
selected = scene.createEntity("Sprite");
selected.addComponent<sophia::SpriteComponent>();
}
ImGui::Separator();
// Flat for now: there is no parent/child component yet.
scene.forEachEntity([this](sophia::Entity entity) {
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf |
ImGuiTreeNodeFlags_NoTreePushOnOpen |
ImGuiTreeNodeFlags_SpanAvailWidth;
if (node.name == selected) {
if (entity == selected) {
flags |= ImGuiTreeNodeFlags_Selected;
}
ImGui::TreeNodeEx(node.name.c_str(), flags);
const std::string& name = entity.getComponent<sophia::TagComponent>().tag;
ImGui::PushID(static_cast<int>(static_cast<sophia::u32>(entity)));
ImGui::TreeNodeEx(name.c_str(), flags);
if (ImGui::IsItemClicked()) {
selected = node.name;
}
return;
}
if (ImGui::TreeNodeEx(node.name.c_str(), ImGuiTreeNodeFlags_SpanAvailWidth)) {
for (const Node& child : node.children) {
drawNode(child);
}
ImGui::TreePop();
}
}
void SceneHierarchyPanel::onUpdate(const sophia::FrameInfo&) {
for (const Node& node : nodes) {
drawNode(node);
selected = entity;
}
ImGui::PopID();
});
}
+8 -20
View File
@@ -1,32 +1,20 @@
#pragma once
#include <sophia/entity.hpp>
#include <sophia/frame_info.hpp>
#include <sophia/scene.hpp>
#include <sophia/ui/panel.hpp>
#include <string>
#include <vector>
class SceneHierarchyPanel : public sophia::Panel {
public:
SceneHierarchyPanel() : Panel{{.name = "Scene Hierarchy", .locked = true}} {}
SceneHierarchyPanel(sophia::Scene& scene, sophia::Entity& selected)
: Panel{{.name = "Scene Hierarchy", .locked = true}},
scene{scene},
selected{selected} {}
void onUpdate(const sophia::FrameInfo& frameInfo) override;
private:
struct Node {
std::string name;
std::vector<Node> children;
};
void drawNode(const Node& node);
std::vector<Node> nodes = {
{"Main Camera", {}},
{"Directional Light", {}},
{"Ground", {}},
{"Player", {}},
{"Enemies", {{"Enemy_01", {}}, {"Enemy_02", {}}, {"Enemy_03", {}}}},
{"Props", {{"Crate_01", {}}, {"Crate_02", {}}}},
};
std::string selected;
sophia::Scene& scene;
sophia::Entity& selected;
};
+169 -13
View File
@@ -2,7 +2,86 @@
#include <imgui.h>
#include <array>
#include <optional>
#include <sophia/components.hpp>
#include <tuple>
namespace {
// Corners of the unit quad a SpriteComponent's model matrix maps to world.
constexpr std::array<glm::vec2, 4> UNIT_QUAD{
glm::vec2{0.f, 0.f}, glm::vec2{1.f, 0.f}, glm::vec2{1.f, 1.f},
glm::vec2{0.f, 1.f}};
const ImU32 SELECTION_COLOR = IM_COL32(255, 170, 40, 255);
} // namespace
ImVec2 ViewportPanel::ViewportTransform::worldToScreen(
const glm::vec3& world) const {
glm::vec4 clip = viewProjection * glm::vec4{world, 1.f};
glm::vec2 ndc = glm::vec2{clip} / clip.w;
// Vulkan NDC has +y pointing down, same as the screen.
return {origin.x + (ndc.x * 0.5f + 0.5f) * size.x,
origin.y + (ndc.y * 0.5f + 0.5f) * size.y};
}
glm::vec2 ViewportPanel::ViewportTransform::screenToWorld(
const ImVec2& screen) const {
glm::vec2 ndc{(screen.x - origin.x) / size.x * 2.f - 1.f,
(screen.y - origin.y) / size.y * 2.f - 1.f};
glm::vec4 world = inverseViewProjection * glm::vec4{ndc, 0.f, 1.f};
return glm::vec2{world} / world.w;
}
void ViewportPanel::onUpdate(const sophia::FrameInfo&) {
drawToolbar();
ImGui::Separator();
const ImVec2 origin = ImGui::GetCursorScreenPos();
const ImVec2 size = ImGui::GetContentRegionAvail();
if (size.x < 1.0f || size.y < 1.0f) {
return;
}
ui.image(target);
// The image doesn't take mouse input, so without this, dragging in it
// would drag the window instead.
ImGui::SetCursorScreenPos(origin);
ImGui::InvisibleButton("##viewport", size);
ImDrawList* drawList = ImGui::GetWindowDrawList();
drawList->PushClipRect(origin, ImVec2(origin.x + size.x, origin.y + size.y),
true);
if (showGrid) {
drawGrid(origin, size);
}
sophia::Entity camera = scene.getActiveCamera();
if (camera && camera.hasComponent<sophia::CameraComponent>()) {
// Use the panel's size rather than the target's: the target is resized
// a frame late, and the mouse is over the panel.
float aspect = size.x / size.y;
glm::mat4 viewProjection =
camera.getComponent<sophia::CameraComponent>().getViewProjection(
camera.getComponent<sophia::TransformComponent>(), aspect);
ViewportTransform viewport{origin, size, viewProjection,
glm::inverse(viewProjection)};
handleInput(viewport);
drawSelection(viewport);
}
drawList->PopClipRect();
ImGui::SetCursorScreenPos(ImVec2(origin.x + 8.0f, origin.y + 8.0f));
ImGui::TextDisabled("%.0f x %.0f", size.x, size.y);
}
void ViewportPanel::drawToolbar() {
const struct {
const char* label;
GizmoMode mode;
@@ -17,20 +96,14 @@ void ViewportPanel::onUpdate(const sophia::FrameInfo&) {
ImGui::SameLine();
}
ImGui::Checkbox("Grid", &showGrid);
}
ImGui::Separator();
const ImVec2 origin = ImGui::GetCursorScreenPos();
const ImVec2 size = ImGui::GetContentRegionAvail();
const ImVec2 end(origin.x + size.x, origin.y + size.y);
ui.image(target);
ImDrawList* drawList = ImGui::GetWindowDrawList();
if (showGrid) {
void ViewportPanel::drawGrid(ImVec2 origin, ImVec2 size) {
constexpr float GRID_STEP = 40.0f;
const ImU32 gridColor = IM_COL32(255, 255, 255, 18);
const ImVec2 end(origin.x + size.x, origin.y + size.y);
ImDrawList* drawList = ImGui::GetWindowDrawList();
for (float x = GRID_STEP; x < size.x; x += GRID_STEP) {
drawList->AddLine(ImVec2(origin.x + x, origin.y),
ImVec2(origin.x + x, end.y), gridColor);
@@ -39,8 +112,91 @@ void ViewportPanel::onUpdate(const sophia::FrameInfo&) {
drawList->AddLine(ImVec2(origin.x, origin.y + y),
ImVec2(end.x, origin.y + y), gridColor);
}
}
void ViewportPanel::handleInput(const ViewportTransform& viewport) {
const ImGuiIO& io = ImGui::GetIO();
if (ImGui::IsItemActivated()) {
selected = pick(viewport.screenToWorld(io.MousePos));
dragging = selected && gizmoMode == GizmoMode::Move;
}
ImGui::SetCursorScreenPos(ImVec2(origin.x + 8.0f, origin.y + 8.0f));
ImGui::TextDisabled("%.0f x %.0f", size.x, size.y);
if (!ImGui::IsItemActive()) {
dragging = false;
return;
}
if (dragging && (io.MouseDelta.x != 0.f || io.MouseDelta.y != 0.f)) {
ImVec2 previous(io.MousePos.x - io.MouseDelta.x,
io.MousePos.y - io.MouseDelta.y);
glm::vec2 delta =
viewport.screenToWorld(io.MousePos) - viewport.screenToWorld(previous);
auto& transform = selected.getComponent<sophia::TransformComponent>();
transform.position.x += delta.x;
transform.position.y += delta.y;
}
}
void ViewportPanel::drawSelection(const ViewportTransform& viewport) {
if (!selected) {
return;
}
ImDrawList* drawList = ImGui::GetWindowDrawList();
const auto& transform = selected.getComponent<sophia::TransformComponent>();
if (selected.hasComponent<sophia::SpriteComponent>()) {
glm::mat4 model =
selected.getComponent<sophia::SpriteComponent>().getModel(transform);
std::array<ImVec2, 4> corners;
for (size_t i = 0; i < UNIT_QUAD.size(); i++) {
corners[i] = viewport.worldToScreen(
glm::vec3{model * glm::vec4{UNIT_QUAD[i], 0.f, 1.f}});
}
drawList->AddQuad(corners[0], corners[1], corners[2], corners[3],
SELECTION_COLOR, 2.0f);
}
// Mark the transform's position (the sprite's pivot), which is also all
// there is to show for entities without a sprite, such as the camera.
ImVec2 center = viewport.worldToScreen(transform.position);
drawList->AddCircle(center, 4.0f, SELECTION_COLOR, 0, 2.0f);
}
sophia::Entity ViewportPanel::pick(glm::vec2 world) {
sophia::Entity best;
std::optional<std::tuple<sophia::s32, float>> bestOrder;
scene.forEachEntity([&](sophia::Entity entity) {
if (!entity.hasComponent<sophia::SpriteComponent>()) {
return;
}
const auto& transform = entity.getComponent<sophia::TransformComponent>();
const auto& sprite = entity.getComponent<sophia::SpriteComponent>();
glm::mat4 model = sprite.getModel(transform);
// Zero size or scale; nothing to hit.
if (glm::determinant(model) == 0.f) {
return;
}
glm::vec4 local =
glm::inverse(model) * glm::vec4{world, transform.position.z, 1.f};
if (local.x < 0.f || local.x > 1.f || local.y < 0.f || local.y > 1.f) {
return;
}
// Same ordering the renderer draws in, so this is the one on top.
std::tuple order{sprite.sortOrder, transform.position.z};
if (!bestOrder || order >= *bestOrder) {
best = entity;
bestOrder = order;
}
});
return best;
}
+39 -2
View File
@@ -1,27 +1,64 @@
#pragma once
#include <sophia/entity.hpp>
#include <sophia/frame_info.hpp>
#include <sophia/handles.hpp>
#include <sophia/math.hpp>
#include <sophia/scene.hpp>
#include <sophia/ui/panel.hpp>
#include <sophia/ui/ui.hpp>
/**
* Shows the scene through its active camera. Click a sprite to select it,
* drag it to move it (in Move mode).
*
* Picking and moving are 2D: they assume an orthographic camera rotated only
* around Z, and work in the z = 0 plane.
*/
class ViewportPanel : public sophia::Panel {
public:
ViewportPanel(sophia::UI& ui, sophia::RenderTargetHandle target)
ViewportPanel(sophia::UI& ui, sophia::RenderTargetHandle target,
sophia::Scene& scene, sophia::Entity& selected)
: Panel{{.name = "Viewport",
.windowFlags = ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse}},
ui{ui},
target{target} {}
target{target},
scene{scene},
selected{selected} {}
void onUpdate(const sophia::FrameInfo& frameInfo) override;
private:
// Where the scene image is on screen this frame, and the camera looking
// through it.
struct ViewportTransform {
ImVec2 origin;
ImVec2 size;
glm::mat4 viewProjection;
glm::mat4 inverseViewProjection;
ImVec2 worldToScreen(const glm::vec3& world) const;
glm::vec2 screenToWorld(const ImVec2& screen) const;
};
void drawToolbar();
void drawGrid(ImVec2 origin, ImVec2 size);
void handleInput(const ViewportTransform& viewport);
void drawSelection(const ViewportTransform& viewport);
// The top-most sprite under `world`, or a null Entity.
sophia::Entity pick(glm::vec2 world);
sophia::UI& ui;
sophia::RenderTargetHandle target;
sophia::Scene& scene;
sophia::Entity& selected;
enum class GizmoMode { Move, Rotate, Scale };
GizmoMode gizmoMode = GizmoMode::Move;
bool showGrid = true;
// Set while a drag that started on the selected entity is moving it.
bool dragging = false;
};
+6
View File
@@ -24,9 +24,15 @@ target_compile_definitions(${ENGINE_NAME} PUBLIC
target_link_libraries(${ENGINE_NAME} PUBLIC
vulkan
vma
shaderc
glfw
glm
imgui
entt
Tracy::TracyClient
)
target_link_libraries(${ENGINE_NAME} PRIVATE
stb_image
)
+1 -1
View File
@@ -47,7 +47,7 @@ class Application {
ResourceRegistryRef resources();
PanelRegistry& panels() { return this->panelRegistry; }
SceneRegistry& scenes() { return this->sceneRegistry; }
SceneViewRegistry& views() { return this->sceneViewRegistry; }
SceneViewRegistry& sceneViews() { return this->sceneViewRegistry; }
UI& ui() { return this->_ui; }
DebugUI& debugUI() { return this->_debugUI; }
+38
View File
@@ -47,6 +47,35 @@ struct MeshComponent {
MeshHandle handle;
};
/**
* A textured quad in the XY plane, placed by the entity's TransformComponent.
* Only rotation around Z is meaningful.
*/
struct SpriteComponent {
// Invalid draws a solid quad in `color`.
TextureHandle texture = INVALID_HANDLE;
glm::vec4 color{1.f, 1.f, 1.f, 1.f};
// Size in world units, before the transform's scale.
glm::vec2 size{1.f, 1.f};
// Point on the sprite, from (0, 0) to (1, 1) across its size, that sits at
// the transform's position and that it rotates around.
glm::vec2 pivot{0.5f, 0.5f};
// Region of the texture to draw, as normalized (u0, v0, u1, v1).
glm::vec4 uvRect{0.f, 0.f, 1.f, 1.f};
// Higher draws on top. Ties are broken by the transform's position.z.
s32 sortOrder = 0;
// Maps the unit quad (0, 0)-(1, 1) to world space: to the sprite's
// rectangle around its pivot, then by `transform`.
glm::mat4 getModel(const TransformComponent& transform) const {
return transform.mat4() *
glm::translate(glm::mat4{1.f}, glm::vec3{-pivot * size, 0.f}) *
glm::scale(glm::mat4{1.f}, glm::vec3{size, 1.f});
}
};
struct CameraComponent {
struct Orthographic {
float height = 10.f;
@@ -77,6 +106,15 @@ struct CameraComponent {
return glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight,
ortho.nearPlane, ortho.farPlane);
}
// World -> clip space for this camera placed by `transform`. The view
// ignores the transform's scale; zoom is the projection's job.
glm::mat4 getViewProjection(const TransformComponent& transform,
float aspect) const {
glm::mat4 world = glm::translate(glm::mat4{1.f}, transform.position) *
glm::mat4_cast(transform.rotation);
return getProjection(aspect) * glm::inverse(world);
}
};
} // namespace sophia
+7
View File
@@ -59,4 +59,11 @@ class Entity {
Scene* scene = nullptr;
};
template <typename Fn>
void Scene::forEachEntity(Fn&& fn) {
for (auto [handle] : registry.storage<entt::entity>().each()) {
fn(Entity{handle, this});
}
}
} // namespace sophia
+2
View File
@@ -38,11 +38,13 @@ class Panel;
class RenderTarget;
class Scene;
struct SceneView;
class Texture;
using MeshHandle = Handle<Mesh>;
using PanelHandle = Handle<Panel>;
using RenderTargetHandle = Handle<RenderTarget>;
using SceneHandle = Handle<Scene>;
using SceneViewHandle = Handle<SceneView>;
using TextureHandle = Handle<Texture>;
} // namespace sophia
@@ -26,8 +26,10 @@ class ResourceRegistryRef {
void requestResize(RenderTargetHandle handle, u32 width, u32 height);
glm::vec2 getExtent(RenderTargetHandle handle) const;
glm::vec2 getExtent(TextureHandle handle) const;
bool isAlive(RenderTargetHandle handle) const;
bool isAlive(TextureHandle handle) const;
template <typename T>
void release(Handle<T> handle);
@@ -45,16 +47,25 @@ class ResourceRegistryRef {
RenderTargetHandle createResource(std::type_identity<RenderTarget>,
u32 width = 1, u32 height = 1);
TextureHandle createResource(std::type_identity<Texture>, u32 width,
u32 height, std::vector<u8> pixels);
ResourceRegistry* registry;
};
template <>
MeshHandle ResourceRegistryRef::load<Mesh>(const std::string& path);
template <>
TextureHandle ResourceRegistryRef::load<Texture>(const std::string& path);
template <>
void ResourceRegistryRef::release<Mesh>(MeshHandle handle);
template <>
void ResourceRegistryRef::release<RenderTarget>(RenderTargetHandle handle);
template <>
void ResourceRegistryRef::release<Texture>(TextureHandle handle);
} // namespace sophia
+9 -5
View File
@@ -13,13 +13,17 @@ class Scene {
Scene();
~Scene();
void createEntity(const std::string& name = {});
Entity 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; }
Entity createCamera(const std::string& name = "Camera");
/** Calls `fn(Entity)` for every live entity. Defined in entity.hpp. */
template <typename Fn>
void forEachEntity(Fn&& fn);
Entity getActiveCamera();
void setActiveCamera(Entity camera);
private:
friend class Entity;
+2
View File
@@ -19,6 +19,8 @@ class SceneRegistry {
const Scene& get(SceneHandle handle) const { return pool.get(handle); }
Scene& get(SceneHandle handle) { return pool.get(handle); }
const Scene& peek(SceneHandle handle) const { return pool.get(handle); }
void release(SceneHandle handle) { pool.release(handle); }
bool isAlive(SceneHandle handle) const { return pool.isAlive(handle); }
+3 -2
View File
@@ -17,8 +17,9 @@ enum class ClearFlags : u8 {
struct SceneView {
SceneHandle scene = INVALID_HANDLE;
RenderTargetHandle target = INVALID_HANDLE;
Entity camera;
RenderTargetHandle target =
INVALID_HANDLE; // null implies render to swapchain
Entity camera; // null implies use scene's active camera
vec4 viewport{0.0f, 0.0f, 1.0f, 1.0f};
@@ -35,6 +35,10 @@ class SceneViewRegistry {
return pool.get(handle);
}
const SceneView& peek(SceneViewHandle handle) const {
return pool.get(handle);
}
void release(SceneViewHandle handle) {
isRenderQueueDirty = true;
pool.release(handle);
+40 -14
View File
@@ -6,18 +6,20 @@
#include <cstddef>
#include <glm/glm.hpp>
#include <map>
#include <set>
#include <sophia/application.hpp>
#include <sophia/util/logger.hpp>
#include <string>
#include <tracy/Tracy.hpp>
#include <utility>
#include <vector>
#include <vulkan/vulkan.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 "renderer/imgui_renderer.hpp"
#include "renderer/model_render_system.hpp"
#include "renderer/render_context.hpp"
#include "renderer/render_graph.hpp"
#include "renderer/scene_renderer.hpp"
#include "resources/mesh.hpp"
#include "resources/render_target.hpp"
#include "resources/resource_registry.hpp"
@@ -65,7 +67,8 @@ class Application::Impl {
void run() {
ModelRenderSystem modelRenderSystem{device, renderContext};
SceneRenderer sceneRenderer{this->application.sceneRegistry,
this->resourceRegistry};
this->resourceRegistry, device,
renderContext.getImageFormat()};
ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(),
renderContext.getImageCount(),
this->resourceRegistry};
@@ -86,11 +89,12 @@ class Application::Impl {
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());
// Views already warned about having no camera, so each warns once.
std::set<SceneViewHandle> viewsWithoutCamera;
auto startTime = std::chrono::high_resolution_clock::now();
auto currentTime = startTime;
u32 frameIndex = 0;
@@ -115,7 +119,10 @@ class Application::Impl {
glm::vec2(static_cast<float>(extent.width),
static_cast<float>(extent.height)),
this->application.camera};
{
ZoneScopedN("Application::onUpdate");
this->application.onUpdate(frameInfo);
}
this->application.camera.setProjection(
-frameInfo.currentFramebufferExtent.x / 2.f,
frameInfo.currentFramebufferExtent.x / 2.f,
@@ -128,9 +135,13 @@ class Application::Impl {
}
imguiRenderer.beginFrame();
sceneRenderer.beginFrame(renderContext.getFrameIndex());
panelRegistry.update(frameInfo);
{
ZoneScopedN("Application::onRender");
this->application.onRender();
}
std::vector<ModelRenderSystem::MeshInstance> instances;
instances.reserve(this->meshes.size() + this->drawCalls.size());
@@ -146,8 +157,8 @@ class Application::Impl {
graph.importImage(renderContext.getCurrentImage(), Access::Present);
std::map<RenderTargetHandle, ImageRef> viewTargets;
for (SceneViewHandle handle : sceneViewRegistryConst.GetRenderQueue()) {
const SceneView& view = sceneViewRegistryConst.get(handle);
for (SceneViewHandle handle : sceneViewRegistry.GetRenderQueue()) {
const SceneView& view = sceneViewRegistry.peek(handle);
if (!sceneRegistry.isAlive(view.scene) ||
!renderTargetPool.isAlive(view.target)) {
continue;
@@ -170,11 +181,24 @@ class Application::Impl {
: 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);
[&sceneRenderer, &sceneRegistry, &sceneViewRegistry,
&viewsWithoutCamera, handle](vk::CommandBuffer commandBuffer) {
ZoneScopedN("scene view pass");
const SceneView& view = sceneViewRegistry.peek(handle);
Entity camera =
view.camera ? view.camera
: sceneRegistry.get(view.scene).getActiveCamera();
// No camera to look through: the target is just cleared.
if (!camera) {
if (viewsWithoutCamera.insert(handle).second) {
log::warning(
"scene view has no camera and its scene has no active "
"camera; it will only be cleared (see "
"Scene::createCamera)");
}
return;
}
sceneRenderer.render(commandBuffer, view, camera);
});
}
@@ -188,6 +212,7 @@ class Application::Impl {
}
},
[&](vk::CommandBuffer commandBuffer) {
ZoneScopedN("main pass");
modelRenderSystem.render(commandBuffer, instances, frameInfo);
imguiRenderer.endFrame(commandBuffer);
@@ -196,6 +221,7 @@ class Application::Impl {
graph.execute(commandBuffer);
renderContext.endFrame();
FrameMark;
frameIndex++;
}
@@ -1,10 +1,12 @@
#include "rendering/imgui_renderer.hpp"
#include "renderer/imgui_renderer.hpp"
#include <imgui.h>
#include <imgui_impl_glfw.h>
#include <imgui_impl_vulkan.h>
#include <imgui_internal.h>
#include <tracy/Tracy.hpp>
#include "resources/render_target.hpp"
namespace sophia {
@@ -79,6 +81,7 @@ ImGuiRenderer::~ImGuiRenderer() {
}
void ImGuiRenderer::beginFrame() {
ZoneScoped;
releaseStaleTextures();
ImGui_ImplVulkan_NewFrame();
@@ -91,12 +94,13 @@ void ImGuiRenderer::beginFrame() {
}
ImTextureID ImGuiRenderer::getTextureId(RenderTargetHandle handle) {
vk::ImageView view =
resources.renderTargetPool.get(handle).getImage().getView();
const RenderTarget& target = resources.renderTargetPool.get(handle);
vk::ImageView view = target.getImage().getView();
auto it = textures.find(handle.index);
if (it != textures.end()) {
if (it->second.generation == handle.generation && it->second.view == view) {
if (it->second.generation == handle.generation &&
it->second.version == target.getVersion()) {
return (ImTextureID)it->second.descriptorSet;
}
ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet);
@@ -107,7 +111,8 @@ ImTextureID ImGuiRenderer::getTextureId(RenderTargetHandle handle) {
// 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};
textures[handle.index] = {handle.generation, target.getVersion(),
descriptorSet};
return (ImTextureID)descriptorSet;
}
@@ -120,7 +125,7 @@ void ImGuiRenderer::releaseStaleTextures() {
bool stale =
!renderTargetPool.isAlive(handle) ||
renderTargetPool.get(handle).getImage().getView() != it->second.view;
renderTargetPool.get(handle).getVersion() != it->second.version;
if (stale) {
ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet);
it = textures.erase(it);
@@ -181,6 +186,7 @@ void ImGuiRenderer::applyLayoutIfNeeded(ImGuiID dockspaceId,
}
void ImGuiRenderer::endFrame(vk::CommandBuffer commandBuffer) {
ZoneScoped;
ImGui::Render();
ImDrawData* mainDrawData = ImGui::GetDrawData();
@@ -37,7 +37,7 @@ class ImGuiRenderer {
struct Texture {
u32 generation;
vk::ImageView view;
u32 version;
VkDescriptorSet descriptorSet;
};
@@ -1,6 +1,7 @@
#include "rendering/model_render_system.hpp"
#include "renderer/model_render_system.hpp"
#include <sophia/util/assert.hpp>
#include <tracy/Tracy.hpp>
#include "shaders/model_render_system.frag.spv.h"
#include "shaders/model_render_system.vert.spv.h"
@@ -44,6 +45,7 @@ ModelRenderSystem::~ModelRenderSystem() {
void ModelRenderSystem::render(vk::CommandBuffer commandBuffer,
std::span<const MeshInstance> instances,
const FrameInfo& frameInfo) {
ZoneScoped;
glm::mat4 viewProjection = frameInfo.camera.getViewProjection();
this->graphicsPipeline->bind(commandBuffer);
@@ -4,7 +4,7 @@
#include <sophia/frame_info.hpp>
#include <span>
#include "rendering/render_context.hpp"
#include "renderer/render_context.hpp"
#include "resources/mesh.hpp"
#include "vulkan/device.hpp"
#include "vulkan/pipelines/graphics_pipeline.hpp"
@@ -1,9 +1,10 @@
#include "rendering/render_context.hpp"
#include "renderer/render_context.hpp"
#include <limits>
#include <sophia/util/assert.hpp>
#include <sophia/util/logger.hpp>
#include <stdexcept>
#include <tracy/Tracy.hpp>
#include "vulkan/access.hpp"
@@ -18,14 +19,19 @@ RenderContext::RenderContext(Window& window, Device& device)
RenderContext::~RenderContext() { this->device.waitIdle(); }
vk::CommandBuffer RenderContext::beginFrame() {
ZoneScoped;
SOPHIA_ASSERT(!this->isFrameStarted,
"Can't call beginFrame while already in progress");
FrameData& frame = this->frames[this->currentFrameIndex];
vk::Fence inFlightFence = frame.inFlight.get();
vk::Result fenceResult = this->device.get()->waitForFences(
vk::Result fenceResult;
{
ZoneScopedN("wait for frame fence");
fenceResult = this->device.get()->waitForFences(
inFlightFence, vk::True, std::numeric_limits<u64>::max());
}
if (fenceResult != vk::Result::eSuccess) {
log::error("failed to wait for fence: " + vk::to_string(fenceResult));
@@ -73,28 +79,35 @@ vk::CommandBuffer RenderContext::beginFrame() {
}
void RenderContext::endFrame() {
ZoneScoped;
SOPHIA_ASSERT(this->isFrameStarted,
"Can't call endFrame while no frame is in progress");
FrameData& frame = this->frames[this->currentFrameIndex];
frame.commandBuffer.end();
vk::Semaphore waitSemaphore = frame.imageAvailable.get();
vk::Semaphore signalSemaphore =
this->renderFinishedSemaphores.at(this->currentImageIndex).get();
vk::PipelineStageFlags waitStage = SWAPCHAIN_ACQUIRE_STAGE;
vk::CommandBuffer commandBuffer = frame.commandBuffer;
vk::SubmitInfo submitInfo{};
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = &waitSemaphore;
submitInfo.pWaitDstStageMask = &waitStage;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = &signalSemaphore;
vk::SemaphoreSubmitInfo waitInfo{};
waitInfo.semaphore = frame.imageAvailable.get();
waitInfo.stageMask = SWAPCHAIN_ACQUIRE_STAGE;
this->device.getGraphicsQueue().submit(submitInfo, frame.inFlight.get());
// Present has no pipeline stage to wait on, so signal once everything in
// the submission is done.
vk::SemaphoreSubmitInfo signalInfo{};
signalInfo.semaphore = signalSemaphore;
signalInfo.stageMask = vk::PipelineStageFlagBits2::eAllCommands;
vk::CommandBufferSubmitInfo commandBufferInfo{};
commandBufferInfo.commandBuffer = frame.commandBuffer;
vk::SubmitInfo2 submitInfo{};
submitInfo.setWaitSemaphoreInfos(waitInfo);
submitInfo.setCommandBufferInfos(commandBufferInfo);
submitInfo.setSignalSemaphoreInfos(signalInfo);
this->device.getGraphicsQueue().submit2(submitInfo, frame.inFlight.get());
vk::Result result = this->swapchain->present(
this->device.getPresentQueue(), this->currentImageIndex, signalSemaphore);
@@ -39,6 +39,7 @@ class RenderContext {
vk::Format getImageFormat() const {
return this->swapchain->getImageFormat();
}
u32 getFrameIndex() const { return this->currentFrameIndex; }
u32 getImageCount() const {
return static_cast<u32>(this->swapchain->imageCount());
}
@@ -1,6 +1,7 @@
#include "rendering/render_graph.hpp"
#include "renderer/render_graph.hpp"
#include <sophia/util/assert.hpp>
#include <tracy/Tracy.hpp>
#include <utility>
#include "vulkan/debug_label.hpp"
@@ -24,6 +25,7 @@ void RenderGraph::addPass(std::string name, const SetupFn& setup,
}
void RenderGraph::execute(vk::CommandBuffer commandBuffer) {
ZoneScoped;
for (Pass& pass : this->passes) {
this->recordPass(commandBuffer, pass);
}
@@ -53,8 +55,6 @@ void RenderGraph::transitionTo(ImageRef ref, const AccessState& 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
@@ -66,12 +66,7 @@ void RenderGraph::transitionTo(ImageRef ref, const AccessState& next,
void RenderGraph::flush(vk::CommandBuffer commandBuffer,
PendingBarriers& pending) {
if (pending.barriers.empty()) {
return;
}
commandBuffer.pipelineBarrier(pending.srcStages, pending.dstStages, {},
nullptr, nullptr, pending.barriers);
recordImageBarriers(commandBuffer, pending.barriers);
pending = PendingBarriers{};
}
@@ -104,9 +104,7 @@ class RenderGraph {
};
struct PendingBarriers {
std::vector<vk::ImageMemoryBarrier> barriers;
vk::PipelineStageFlags srcStages;
vk::PipelineStageFlags dstStages;
std::vector<vk::ImageMemoryBarrier2> barriers;
};
void transitionTo(ImageRef ref, const AccessState& next,
+87
View File
@@ -0,0 +1,87 @@
#include "renderer/scene_renderer.hpp"
#include <algorithm>
#include <sophia/components.hpp>
#include <sophia/util/assert.hpp>
#include <tracy/Tracy.hpp>
#include <tuple>
namespace sophia {
SceneRenderer::SceneRenderer(const SceneRegistry& sceneRegistry,
ResourceRegistry& resources, Device& device,
vk::Format colorFormat)
: sceneRegistry{sceneRegistry},
resources{resources},
spriteRenderSystem{device, colorFormat, resources} {}
void SceneRenderer::beginFrame(u32 frameIndex) {
ZoneScoped;
this->spriteRenderSystem.beginFrame(frameIndex);
}
void SceneRenderer::render(vk::CommandBuffer commandBuffer,
const SceneView& sceneView, Entity camera) {
ZoneScoped;
SOPHIA_ASSERT(camera.hasComponent<CameraComponent>(),
"camera does not have camera component!");
vk::Extent2D extent =
this->resources.renderTargetPool.get(sceneView.target).getExtent();
float aspect =
static_cast<float>(extent.width) / static_cast<float>(extent.height);
glm::mat4 viewProjection =
camera.getComponent<CameraComponent>().getViewProjection(
camera.getComponent<TransformComponent>(), aspect);
renderSprites(commandBuffer, this->sceneRegistry.get(sceneView.scene),
viewProjection);
// TODO: meshes. Group MeshComponent entities by mesh and draw each group
// (instanced), as sprites are grouped by texture.
}
void SceneRenderer::renderSprites(vk::CommandBuffer commandBuffer,
const Scene& scene,
const glm::mat4& viewProjection) {
this->spriteDrawItems.clear();
auto view =
scene.registry.view<const TransformComponent, const SpriteComponent>();
for (auto [entity, transform, sprite] : view.each()) {
TextureHandle texture = this->resources.texturePool.isAlive(sprite.texture)
? sprite.texture
: this->resources.whiteTexture;
this->spriteDrawItems.push_back(
{sprite.sortOrder,
transform.position.z,
texture,
{sprite.getModel(transform), sprite.color, sprite.uvRect}});
}
// Texture is the last key so that sprites whose order doesn't matter
// relative to each other still end up in as few batches as possible.
std::ranges::sort(this->spriteDrawItems, {}, [](const SpriteDrawItem& item) {
return std::tuple{item.sortOrder, item.depth, item.texture};
});
this->spriteInstances.clear();
this->spriteBatches.clear();
for (const SpriteDrawItem& item : this->spriteDrawItems) {
auto index = static_cast<u32>(this->spriteInstances.size());
this->spriteInstances.push_back(item.instance);
if (this->spriteBatches.empty() ||
this->spriteBatches.back().texture != item.texture) {
this->spriteBatches.push_back({item.texture, index, 0});
}
this->spriteBatches.back().instanceCount++;
}
this->spriteRenderSystem.render(commandBuffer, viewProjection,
this->spriteInstances, this->spriteBatches);
}
} // namespace sophia
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include <sophia/scene_registry.hpp>
#include <sophia/scene_view.hpp>
#include <vector>
#include "renderer/sprite_render_system.hpp"
#include "resources/resource_registry.hpp"
namespace sophia {
/**
* !TEMP to be decoupled once scene batching and rendering impl
*
* Turns a scene's components into draw calls for a SceneView.
*/
class SceneRenderer {
public:
SceneRenderer(const SceneRenderer&) = delete;
SceneRenderer& operator=(const SceneRenderer&) = delete;
SceneRenderer(const SceneRegistry& sceneRegistry, ResourceRegistry& resources,
Device& device, vk::Format colorFormat);
// Call once per frame, before any render().
void beginFrame(u32 frameIndex);
// Records `sceneView` as seen through `camera` (an entity of the view's
// scene with a CameraComponent) into `commandBuffer`. Rendering into the
// view's target must already have begun.
void render(vk::CommandBuffer commandBuffer, const SceneView& sceneView,
Entity camera);
private:
struct SpriteDrawItem {
s32 sortOrder;
float depth;
TextureHandle texture;
SpriteRenderSystem::Instance instance;
};
void renderSprites(vk::CommandBuffer commandBuffer, const Scene& scene,
const glm::mat4& viewProjection);
const SceneRegistry& sceneRegistry;
ResourceRegistry& resources;
SpriteRenderSystem spriteRenderSystem;
// Reused across frames to avoid reallocating.
std::vector<SpriteDrawItem> spriteDrawItems;
std::vector<SpriteRenderSystem::Instance> spriteInstances;
std::vector<SpriteRenderSystem::Batch> spriteBatches;
};
} // namespace sophia
@@ -0,0 +1,196 @@
#include "renderer/sprite_render_system.hpp"
#include <sophia/util/assert.hpp>
#include <sophia/util/logger.hpp>
#include <tracy/Tracy.hpp>
#include "shaders/sprite.frag.spv.h"
#include "shaders/sprite.vert.spv.h"
#include "vulkan/descriptors/descriptor_writer.hpp"
namespace sophia {
namespace {
constexpr u32 MAX_TEXTURES = 1024;
constexpr u32 VERTICES_PER_SPRITE = 6;
} // namespace
SpriteRenderSystem::SpriteRenderSystem(Device& device, vk::Format colorFormat,
ResourceRegistry& resources)
: device{device}, resources{resources} {
createDescriptors();
createPipelineLayout();
createPipeline(colorFormat);
createInstanceBuffers();
}
SpriteRenderSystem::~SpriteRenderSystem() {
log::trace("destroyed sophia::SpriteRenderSystem");
}
void SpriteRenderSystem::beginFrame(u32 frameIndex) {
this->frameIndex = frameIndex;
this->instanceCursor = 0;
}
void SpriteRenderSystem::render(vk::CommandBuffer commandBuffer,
const glm::mat4& viewProjection,
std::span<const Instance> instances,
std::span<const Batch> batches) {
ZoneScoped;
if (instances.empty()) {
return;
}
u32 available = MAX_INSTANCES_PER_FRAME - this->instanceCursor;
if (instances.size() > available) {
log::warning("SpriteRenderSystem: frame instance buffer full, dropping",
instances.size() - available, "sprites");
}
Buffer& buffer = *this->instanceBuffers[this->frameIndex];
u32 base = this->instanceCursor;
u32 count = std::min(static_cast<u32>(instances.size()), available);
buffer.writeToBuffer(const_cast<Instance*>(instances.data()),
count * sizeof(Instance), base * sizeof(Instance));
this->instanceCursor += count;
this->graphicsPipeline->bind(commandBuffer);
PushConstantData push{viewProjection};
commandBuffer.pushConstants(this->pipelineLayout.get(),
vk::ShaderStageFlagBits::eVertex, 0,
sizeof(PushConstantData), &push);
vk::Buffer vertexBuffer = buffer.getBuffer();
vk::DeviceSize offset = 0;
commandBuffer.bindVertexBuffers(0, 1, &vertexBuffer, &offset);
for (const Batch& batch : batches) {
if (batch.firstInstance >= count) {
break;
}
u32 batchCount = std::min(batch.instanceCount, count - batch.firstInstance);
vk::DescriptorSet set = getDescriptorSet(batch.texture);
commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
this->pipelineLayout.get(), 0, 1, &set, 0,
nullptr);
commandBuffer.draw(VERTICES_PER_SPRITE, batchCount, 0,
base + batch.firstInstance);
}
}
vk::DescriptorSet SpriteRenderSystem::getDescriptorSet(TextureHandle handle) {
auto it = this->descriptorSets.find(handle.index);
if (it != this->descriptorSets.end() &&
it->second.generation == handle.generation) {
return it->second.set;
}
const Texture& texture = this->resources.texturePool.get(handle);
vk::DescriptorImageInfo imageInfo = texture.descriptorInfo();
DescriptorWriter writer{*this->setLayout, *this->descriptorPool};
writer.writeImage(0, &imageInfo);
if (it == this->descriptorSets.end()) {
vk::DescriptorSet set;
bool allocated = writer.build(set);
SOPHIA_ASSERT(allocated, "SpriteRenderSystem: out of texture descriptors");
this->descriptorSets.emplace(static_cast<u32>(handle.index),
CachedDescriptorSet{handle.generation, set});
return set;
}
// The slot was reused by a new texture. Releasing a texture waits for the
// device to go idle, so no in-flight frame can still be using this set.
writer.overwrite(it->second.set);
it->second.generation = handle.generation;
return it->second.set;
}
void SpriteRenderSystem::createDescriptors() {
this->setLayout =
DescriptorSetLayout::Builder(this->device)
.addBinding(0, vk::DescriptorType::eCombinedImageSampler,
vk::ShaderStageFlagBits::eFragment)
.build();
this->descriptorPool =
DescriptorPool::Builder(this->device)
.setMaxSets(MAX_TEXTURES)
.addPoolSize(vk::DescriptorType::eCombinedImageSampler, MAX_TEXTURES)
.build();
}
void SpriteRenderSystem::createPipelineLayout() {
vk::PushConstantRange pushConstantRange{};
pushConstantRange.stageFlags = vk::ShaderStageFlagBits::eVertex;
pushConstantRange.offset = 0;
pushConstantRange.size = sizeof(PushConstantData);
vk::DescriptorSetLayout setLayout = this->setLayout->getDescriptorSetLayout();
vk::PipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &setLayout;
pipelineLayoutInfo.pushConstantRangeCount = 1;
pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange;
try {
this->pipelineLayout =
this->device.get()->createPipelineLayoutUnique(pipelineLayoutInfo);
log::trace("created vk::PipelineLayout");
} catch (const vk::SystemError& err) {
log::fatal("failed to create vk::PipelineLayout");
throw std::runtime_error("failed to create vk::PipelineLayout");
}
}
void SpriteRenderSystem::createPipeline(vk::Format colorFormat) {
auto vertexShader = Shader::Builder()
.fromSPIRV(shaders::sprite_vert_spv)
.setStage(ShaderStage::VERTEX)
.build();
auto fragmentShader = Shader::Builder()
.fromSPIRV(shaders::sprite_frag_spv)
.setStage(ShaderStage::FRAGMENT)
.build();
std::vector<vk::VertexInputBindingDescription> bindings{
{0, sizeof(Instance), vk::VertexInputRate::eInstance}};
std::vector<vk::VertexInputAttributeDescription> attributes;
// A mat4 attribute takes four consecutive locations, one per column.
for (u32 column = 0; column < 4; column++) {
attributes.push_back({column, 0, vk::Format::eR32G32B32A32Sfloat,
static_cast<u32>(offsetof(Instance, model) +
column * sizeof(glm::vec4))});
}
attributes.push_back({4, 0, vk::Format::eR32G32B32A32Sfloat,
static_cast<u32>(offsetof(Instance, color))});
attributes.push_back({5, 0, vk::Format::eR32G32B32A32Sfloat,
static_cast<u32>(offsetof(Instance, uvRect))});
this->graphicsPipeline = GraphicsPipeline::Builder(this->device)
.setVertexShader(*vertexShader)
.setFragmentShader(*fragmentShader)
.setPipelineLayout(this->pipelineLayout.get())
.setColorAttachmentFormats({colorFormat})
.setVertexInput(bindings, attributes)
.setAlphaBlending()
.build();
}
void SpriteRenderSystem::createInstanceBuffers() {
for (std::unique_ptr<Buffer>& buffer : this->instanceBuffers) {
buffer = std::make_unique<Buffer>(
this->device, sizeof(Instance), MAX_INSTANCES_PER_FRAME,
vk::BufferUsageFlagBits::eVertexBuffer,
vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent);
buffer->map();
}
}
} // namespace sophia
@@ -0,0 +1,94 @@
#pragma once
#include <array>
#include <memory>
#include <sophia/handles.hpp>
#include <sophia/math.hpp>
#include <span>
#include <unordered_map>
#include "renderer/render_context.hpp"
#include "resources/resource_registry.hpp"
#include "vulkan/buffer.hpp"
#include "vulkan/descriptors/descriptor_pool.hpp"
#include "vulkan/descriptors/descriptor_set_layout.hpp"
#include "vulkan/device.hpp"
#include "vulkan/pipelines/graphics_pipeline.hpp"
namespace sophia {
/**
* Draws batches of textured quads. Knows nothing about scenes: callers hand
* it instances already in draw order, grouped into runs that share a texture.
*
* Instances are written to a per-frame host-visible buffer, so every
* render() call within one frame appends to it; call beginFrame() once per
* frame before any render().
*/
class SpriteRenderSystem {
public:
// Layout must match the per-instance inputs in sprite.vert.
struct Instance {
glm::mat4 model{1.f};
glm::vec4 color{1.f};
glm::vec4 uvRect{0.f, 0.f, 1.f, 1.f};
};
// A run of consecutive instances drawn with the same texture.
struct Batch {
TextureHandle texture;
u32 firstInstance;
u32 instanceCount;
};
static constexpr u32 MAX_INSTANCES_PER_FRAME = 32768;
SpriteRenderSystem(const SpriteRenderSystem&) = delete;
SpriteRenderSystem& operator=(const SpriteRenderSystem&) = delete;
SpriteRenderSystem(Device& device, vk::Format colorFormat,
ResourceRegistry& resources);
~SpriteRenderSystem();
void beginFrame(u32 frameIndex);
void render(vk::CommandBuffer commandBuffer, const glm::mat4& viewProjection,
std::span<const Instance> instances,
std::span<const Batch> batches);
private:
struct PushConstantData {
glm::mat4 viewProjection{1.f};
};
struct CachedDescriptorSet {
u32 generation;
vk::DescriptorSet set;
};
void createDescriptors();
void createPipelineLayout();
void createPipeline(vk::Format colorFormat);
void createInstanceBuffers();
vk::DescriptorSet getDescriptorSet(TextureHandle handle);
Device& device;
ResourceRegistry& resources;
std::unique_ptr<DescriptorSetLayout> setLayout;
std::unique_ptr<DescriptorPool> descriptorPool;
// One set per texture slot, keyed by handle index. A generation mismatch
// means the slot was reused by a new texture, so the set is rewritten.
std::unordered_map<u32, CachedDescriptorSet> descriptorSets;
vk::UniquePipelineLayout pipelineLayout;
std::unique_ptr<GraphicsPipeline> graphicsPipeline;
std::array<std::unique_ptr<Buffer>, RenderContext::MAX_FRAMES_IN_FLIGHT>
instanceBuffers;
u32 frameIndex = 0;
u32 instanceCursor = 0;
};
} // namespace sophia
-74
View File
@@ -1,74 +0,0 @@
#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
@@ -1,29 +0,0 @@
#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,5 +1,7 @@
#include "resources/pools/render_target_pool.hpp"
#include <tracy/Tracy.hpp>
namespace sophia {
RenderTargetPool::RenderTargetPool(Device& device, vk::Format format)
@@ -16,6 +18,7 @@ void RenderTargetPool::release(RenderTargetHandle handle) {
}
void RenderTargetPool::applyPendingResizes() {
ZoneScoped;
pool.forEach([](RenderTarget& target) { target.applyPendingResize(); });
}
+1
View File
@@ -39,6 +39,7 @@ void RenderTarget::applyPendingResize() {
}
void RenderTarget::create(u32 width, u32 height) {
this->version++;
this->image = Image::Builder(this->device)
.setExtent(width, height)
.setFormat(this->format)
+2
View File
@@ -27,6 +27,7 @@ class RenderTarget {
Image& getImage() const { return *this->image; }
vk::Extent2D getExtent() const;
u32 getVersion() const { return this->version; }
void requestResize(u32 width, u32 height);
@@ -46,6 +47,7 @@ class RenderTarget {
std::unique_ptr<Image> image;
std::optional<vk::Extent2D> pendingExtent;
u32 version = 0;
};
} // namespace sophia
+11 -2
View File
@@ -1,10 +1,12 @@
#pragma once
#include <array>
#include <sophia/handles.hpp>
#include <sophia/util/pool.hpp>
#include "resources/mesh.hpp"
#include "resources/pools/render_target_pool.hpp"
#include "resources/texture.hpp"
#include "vulkan/device.hpp"
namespace sophia {
@@ -15,13 +17,20 @@ class ResourceRegistry {
ResourceRegistry& operator=(const ResourceRegistry&) = delete;
ResourceRegistry(Device& device, vk::Format renderTargetFormat)
: device{device}, renderTargetPool{device, renderTargetFormat} {}
: device{device}, renderTargetPool{device, renderTargetFormat} {
constexpr std::array<u8, 4> WHITE{0xFF, 0xFF, 0xFF, 0xFF};
whiteTexture = texturePool.create(device, 1, 1, WHITE);
}
Device& device;
Pool<Mesh> meshPool{"Mesh"};
RenderTargetPool renderTargetPool;
// TexturePool textures;
Pool<Texture> texturePool{"Texture"};
// MaterialPool materials;
// 1x1 white. Stands in for sprites without a texture, so they draw as a
// solid quad in their tint color.
TextureHandle whiteTexture = INVALID_HANDLE;
};
} // namespace sophia
@@ -1,5 +1,9 @@
#include <stb_image.h>
#include <sophia/resource_registry_ref.hpp>
#include <sophia/util/assert.hpp>
#include <sophia/util/logger.hpp>
#include <stdexcept>
#include "resources/resource_registry.hpp"
@@ -14,6 +18,26 @@ MeshHandle ResourceRegistryRef::load<Mesh>(const std::string& path) {
return MeshHandle{};
}
template <>
TextureHandle ResourceRegistryRef::load<Texture>(const std::string& path) {
int width = 0;
int height = 0;
int channels = 0;
// Always decode to RGBA8, whatever the file has.
stbi_uc* pixels = stbi_load(path.c_str(), &width, &height, &channels, 4);
if (pixels == nullptr) {
log::fatal("failed to load texture", path, "-", stbi_failure_reason());
throw std::runtime_error("failed to load texture: " + path);
}
auto size = static_cast<size_t>(width) * static_cast<size_t>(height) * 4;
TextureHandle handle = registry->texturePool.create(
registry->device, static_cast<u32>(width), static_cast<u32>(height),
std::span<const u8>{pixels, size});
stbi_image_free(pixels);
return handle;
}
template <>
void ResourceRegistryRef::release<Mesh>(MeshHandle handle) {
registry->meshPool.release(handle);
@@ -24,6 +48,15 @@ void ResourceRegistryRef::release<RenderTarget>(RenderTargetHandle handle) {
registry->renderTargetPool.release(handle);
}
template <>
void ResourceRegistryRef::release<Texture>(TextureHandle handle) {
SOPHIA_ASSERT(handle != registry->whiteTexture,
"the built-in white texture cannot be released");
// Frames in flight may still be sampling it.
registry->device.waitIdle();
registry->texturePool.release(handle);
}
MeshHandle ResourceRegistryRef::createResource(
std::type_identity<Mesh>, std::vector<CanvasVertex> vertices,
std::vector<u32> indices) {
@@ -42,6 +75,13 @@ RenderTargetHandle ResourceRegistryRef::createResource(
return registry->renderTargetPool.create(width, height);
}
TextureHandle ResourceRegistryRef::createResource(std::type_identity<Texture>,
u32 width, u32 height,
std::vector<u8> pixels) {
return registry->texturePool.create(registry->device, width, height,
std::span<const u8>{pixels});
}
void ResourceRegistryRef::requestResize(RenderTargetHandle handle, u32 width,
u32 height) {
registry->renderTargetPool.get(handle).requestResize(width, height);
@@ -56,4 +96,14 @@ bool ResourceRegistryRef::isAlive(RenderTargetHandle handle) const {
return registry->renderTargetPool.isAlive(handle);
}
glm::vec2 ResourceRegistryRef::getExtent(TextureHandle handle) const {
const Texture& texture = registry->texturePool.get(handle);
return {static_cast<float>(texture.getWidth()),
static_cast<float>(texture.getHeight())};
}
bool ResourceRegistryRef::isAlive(TextureHandle handle) const {
return registry->texturePool.isAlive(handle);
}
} // namespace sophia
+32
View File
@@ -0,0 +1,32 @@
#include "resources/texture.hpp"
#include <sophia/util/assert.hpp>
#include <sophia/util/logger.hpp>
namespace sophia {
Texture::Texture(Device& device, u32 width, u32 height,
std::span<const u8> pixels) {
SOPHIA_ASSERT(width > 0 && height > 0, "texture must not be empty");
SOPHIA_ASSERT(pixels.size() == static_cast<size_t>(width) * height * 4,
"texture pixel data must be width * height RGBA8 texels");
this->image = Image::Builder(device)
.setExtent(width, height)
.setFormat(vk::Format::eR8G8B8A8Srgb)
.setUsage(vk::ImageUsageFlagBits::eSampled)
.setInitialData(pixels.data(), pixels.size())
.withView()
.withSampler(vk::Filter::eLinear,
vk::SamplerAddressMode::eClampToEdge)
.build();
}
Texture::~Texture() { log::trace("destroyed sophia::Texture"); }
vk::DescriptorImageInfo Texture::descriptorInfo() const {
return {this->image->getSampler(), this->image->getView(),
vk::ImageLayout::eShaderReadOnlyOptimal};
}
} // namespace sophia
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <memory>
#include <sophia/types.hpp>
#include <span>
#include <vulkan/vulkan.hpp>
#include "vulkan/device.hpp"
#include "vulkan/image.hpp"
namespace sophia {
/**
* A sampled, read-only RGBA8 (sRGB) image.
*
* Owned by the ResourceRegistry and addressed through a TextureHandle.
*/
class Texture {
public:
/** `pixels` is width * height tightly packed RGBA8 texels, top row first. */
Texture(Device& device, u32 width, u32 height, std::span<const u8> pixels);
~Texture();
Texture(const Texture&) = delete;
Texture& operator=(const Texture&) = delete;
const Image& getImage() const { return *this->image; }
u32 getWidth() const { return this->image->getExtent().width; }
u32 getHeight() const { return this->image->getExtent().height; }
vk::DescriptorImageInfo descriptorInfo() const;
private:
std::unique_ptr<Image> image;
};
} // namespace sophia
+34 -6
View File
@@ -1,16 +1,44 @@
#include <sophia/components.hpp>
#include <sophia/entity.hpp>
#include <sophia/scene.hpp>
#include <sophia/util/logger.hpp>
namespace sophia {
Scene::Scene() {
entt::entity entity = this->registry.create();
auto& a = this->registry.emplace<TransformComponent>(entity);
auto& b = this->registry.get<TransformComponent>(entity);
}
Scene::Scene() {}
Scene::~Scene() { log::trace("destroying sophia::Scene"); }
Entity Scene::createEntity(const std::string& name) {
Entity entity{this->registry.create(), this};
entity.addComponent<TagComponent>(name.empty() ? "Entity" : name);
entity.addComponent<TransformComponent>();
return entity;
}
Entity Scene::createCamera(const std::string& name) {
Entity camera = createEntity(name);
camera.addComponent<CameraComponent>();
if (this->activeCamera == entt::null) {
this->activeCamera = camera;
}
return camera;
}
Entity Scene::getActiveCamera() {
if (this->activeCamera == entt::null) {
return {};
}
return {this->activeCamera, this};
}
void Scene::setActiveCamera(Entity camera) { this->activeCamera = camera; }
void Scene::destroyEntity(Entity entity) {
if (this->activeCamera == entity) {
this->activeCamera = entt::null;
}
this->registry.destroy(entity);
}
} // namespace sophia
+12
View File
@@ -0,0 +1,12 @@
#version 450
layout (location = 0) in vec4 fragColor;
layout (location = 1) in vec2 fragUv;
layout (location = 0) out vec4 outColor;
layout (set = 0, binding = 0) uniform sampler2D spriteTexture;
void main() {
outColor = texture(spriteTexture, fragUv) * fragColor;
}
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <array>
#include <cstdint>
namespace sophia::shaders {
inline constexpr std::array<uint32_t, 186> sprite_frag_spv = {
0x07230203, 0x00010000, 0x000d000b, 0x00000018, 0x00000000, 0x00020011,
0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e,
0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x0008000f, 0x00000004,
0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x00000011, 0x00000015,
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, 0x00060005, 0x0000000d, 0x69727073,
0x65546574, 0x72757478, 0x00000065, 0x00040005, 0x00000011, 0x67617266,
0x00007655, 0x00050005, 0x00000015, 0x67617266, 0x6f6c6f43, 0x00000072,
0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00040047, 0x0000000d,
0x00000021, 0x00000000, 0x00040047, 0x0000000d, 0x00000022, 0x00000000,
0x00040047, 0x00000011, 0x0000001e, 0x00000001, 0x00040047, 0x00000015,
0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003,
0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007,
0x00000006, 0x00000004, 0x00040020, 0x00000008, 0x00000003, 0x00000007,
0x0004003b, 0x00000008, 0x00000009, 0x00000003, 0x00090019, 0x0000000a,
0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001,
0x00000000, 0x0003001b, 0x0000000b, 0x0000000a, 0x00040020, 0x0000000c,
0x00000000, 0x0000000b, 0x0004003b, 0x0000000c, 0x0000000d, 0x00000000,
0x00040017, 0x0000000f, 0x00000006, 0x00000002, 0x00040020, 0x00000010,
0x00000001, 0x0000000f, 0x0004003b, 0x00000010, 0x00000011, 0x00000001,
0x00040020, 0x00000014, 0x00000001, 0x00000007, 0x0004003b, 0x00000014,
0x00000015, 0x00000001, 0x00050036, 0x00000002, 0x00000004, 0x00000000,
0x00000003, 0x000200f8, 0x00000005, 0x0004003d, 0x0000000b, 0x0000000e,
0x0000000d, 0x0004003d, 0x0000000f, 0x00000012, 0x00000011, 0x00050057,
0x00000007, 0x00000013, 0x0000000e, 0x00000012, 0x0004003d, 0x00000007,
0x00000016, 0x00000015, 0x00050085, 0x00000007, 0x00000017, 0x00000013,
0x00000016, 0x0003003e, 0x00000009, 0x00000017, 0x000100fd, 0x00010038,
};
} // namespace sophia::shaders
+31
View File
@@ -0,0 +1,31 @@
#version 450
// Per instance; layout must match SpriteRenderSystem::Instance.
layout (location = 0) in mat4 model; // Maps the unit quad to world space.
layout (location = 4) in vec4 color;
layout (location = 5) in vec4 uvRect;
layout (location = 0) out vec4 fragColor;
layout (location = 1) out vec2 fragUv;
layout (push_constant) uniform Push {
mat4 viewProjection;
} push;
// Two triangles covering the unit quad (0,0)-(1,1).
const vec2 CORNERS[6] = vec2[](
vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(1.0, 1.0),
vec2(1.0, 1.0), vec2(0.0, 1.0), vec2(0.0, 0.0)
);
void main() {
vec2 corner = CORNERS[gl_VertexIndex];
gl_Position = push.viewProjection * model * vec4(corner, 0.0, 1.0);
// Draw order comes from the CPU-side sort, so flatten depth; sprites should
// never be clipped by the camera's near/far planes.
gl_Position.z = 0.0;
fragColor = color;
fragUv = mix(uvRect.xy, uvRect.zw, corner);
}
+98
View File
@@ -0,0 +1,98 @@
#pragma once
#include <array>
#include <cstdint>
namespace sophia::shaders {
inline constexpr std::array<uint32_t, 519> sprite_vert_spv = {
0x07230203, 0x00010000, 0x000d000b, 0x00000045, 0x00000000, 0x00020011,
0x00000001, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e,
0x00000000, 0x0003000e, 0x00000000, 0x00000001, 0x000c000f, 0x00000000,
0x00000004, 0x6e69616d, 0x00000000, 0x00000016, 0x00000021, 0x0000002b,
0x00000038, 0x0000003a, 0x0000003d, 0x0000003e, 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, 0x00040005,
0x00000009, 0x6e726f63, 0x00007265, 0x00060005, 0x00000016, 0x565f6c67,
0x65747265, 0x646e4978, 0x00007865, 0x00050005, 0x00000019, 0x65646e69,
0x6c626178, 0x00000065, 0x00060005, 0x0000001f, 0x505f6c67, 0x65567265,
0x78657472, 0x00000000, 0x00060006, 0x0000001f, 0x00000000, 0x505f6c67,
0x7469736f, 0x006e6f69, 0x00070006, 0x0000001f, 0x00000001, 0x505f6c67,
0x746e696f, 0x657a6953, 0x00000000, 0x00070006, 0x0000001f, 0x00000002,
0x435f6c67, 0x4470696c, 0x61747369, 0x0065636e, 0x00070006, 0x0000001f,
0x00000003, 0x435f6c67, 0x446c6c75, 0x61747369, 0x0065636e, 0x00030005,
0x00000021, 0x00000000, 0x00040005, 0x00000024, 0x68737550, 0x00000000,
0x00070006, 0x00000024, 0x00000000, 0x77656976, 0x6a6f7250, 0x69746365,
0x00006e6f, 0x00040005, 0x00000026, 0x68737570, 0x00000000, 0x00040005,
0x0000002b, 0x65646f6d, 0x0000006c, 0x00050005, 0x00000038, 0x67617266,
0x6f6c6f43, 0x00000072, 0x00040005, 0x0000003a, 0x6f6c6f63, 0x00000072,
0x00040005, 0x0000003d, 0x67617266, 0x00007655, 0x00040005, 0x0000003e,
0x65527675, 0x00007463, 0x00040047, 0x00000016, 0x0000000b, 0x0000002a,
0x00030047, 0x0000001f, 0x00000002, 0x00050048, 0x0000001f, 0x00000000,
0x0000000b, 0x00000000, 0x00050048, 0x0000001f, 0x00000001, 0x0000000b,
0x00000001, 0x00050048, 0x0000001f, 0x00000002, 0x0000000b, 0x00000003,
0x00050048, 0x0000001f, 0x00000003, 0x0000000b, 0x00000004, 0x00030047,
0x00000024, 0x00000002, 0x00040048, 0x00000024, 0x00000000, 0x00000005,
0x00050048, 0x00000024, 0x00000000, 0x00000007, 0x00000010, 0x00050048,
0x00000024, 0x00000000, 0x00000023, 0x00000000, 0x00040047, 0x0000002b,
0x0000001e, 0x00000000, 0x00040047, 0x00000038, 0x0000001e, 0x00000000,
0x00040047, 0x0000003a, 0x0000001e, 0x00000004, 0x00040047, 0x0000003d,
0x0000001e, 0x00000001, 0x00040047, 0x0000003e, 0x0000001e, 0x00000005,
0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016,
0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000002,
0x00040020, 0x00000008, 0x00000007, 0x00000007, 0x00040015, 0x0000000a,
0x00000020, 0x00000000, 0x0004002b, 0x0000000a, 0x0000000b, 0x00000006,
0x0004001c, 0x0000000c, 0x00000007, 0x0000000b, 0x0004002b, 0x00000006,
0x0000000d, 0x00000000, 0x0005002c, 0x00000007, 0x0000000e, 0x0000000d,
0x0000000d, 0x0004002b, 0x00000006, 0x0000000f, 0x3f800000, 0x0005002c,
0x00000007, 0x00000010, 0x0000000f, 0x0000000d, 0x0005002c, 0x00000007,
0x00000011, 0x0000000f, 0x0000000f, 0x0005002c, 0x00000007, 0x00000012,
0x0000000d, 0x0000000f, 0x0009002c, 0x0000000c, 0x00000013, 0x0000000e,
0x00000010, 0x00000011, 0x00000011, 0x00000012, 0x0000000e, 0x00040015,
0x00000014, 0x00000020, 0x00000001, 0x00040020, 0x00000015, 0x00000001,
0x00000014, 0x0004003b, 0x00000015, 0x00000016, 0x00000001, 0x00040020,
0x00000018, 0x00000007, 0x0000000c, 0x00040017, 0x0000001c, 0x00000006,
0x00000004, 0x0004002b, 0x0000000a, 0x0000001d, 0x00000001, 0x0004001c,
0x0000001e, 0x00000006, 0x0000001d, 0x0006001e, 0x0000001f, 0x0000001c,
0x00000006, 0x0000001e, 0x0000001e, 0x00040020, 0x00000020, 0x00000003,
0x0000001f, 0x0004003b, 0x00000020, 0x00000021, 0x00000003, 0x0004002b,
0x00000014, 0x00000022, 0x00000000, 0x00040018, 0x00000023, 0x0000001c,
0x00000004, 0x0003001e, 0x00000024, 0x00000023, 0x00040020, 0x00000025,
0x00000009, 0x00000024, 0x0004003b, 0x00000025, 0x00000026, 0x00000009,
0x00040020, 0x00000027, 0x00000009, 0x00000023, 0x00040020, 0x0000002a,
0x00000001, 0x00000023, 0x0004003b, 0x0000002a, 0x0000002b, 0x00000001,
0x00040020, 0x00000033, 0x00000003, 0x0000001c, 0x0004002b, 0x0000000a,
0x00000035, 0x00000002, 0x00040020, 0x00000036, 0x00000003, 0x00000006,
0x0004003b, 0x00000033, 0x00000038, 0x00000003, 0x00040020, 0x00000039,
0x00000001, 0x0000001c, 0x0004003b, 0x00000039, 0x0000003a, 0x00000001,
0x00040020, 0x0000003c, 0x00000003, 0x00000007, 0x0004003b, 0x0000003c,
0x0000003d, 0x00000003, 0x0004003b, 0x00000039, 0x0000003e, 0x00000001,
0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8,
0x00000005, 0x0004003b, 0x00000008, 0x00000009, 0x00000007, 0x0004003b,
0x00000018, 0x00000019, 0x00000007, 0x0004003d, 0x00000014, 0x00000017,
0x00000016, 0x0003003e, 0x00000019, 0x00000013, 0x00050041, 0x00000008,
0x0000001a, 0x00000019, 0x00000017, 0x0004003d, 0x00000007, 0x0000001b,
0x0000001a, 0x0003003e, 0x00000009, 0x0000001b, 0x00050041, 0x00000027,
0x00000028, 0x00000026, 0x00000022, 0x0004003d, 0x00000023, 0x00000029,
0x00000028, 0x0004003d, 0x00000023, 0x0000002c, 0x0000002b, 0x00050092,
0x00000023, 0x0000002d, 0x00000029, 0x0000002c, 0x0004003d, 0x00000007,
0x0000002e, 0x00000009, 0x00050051, 0x00000006, 0x0000002f, 0x0000002e,
0x00000000, 0x00050051, 0x00000006, 0x00000030, 0x0000002e, 0x00000001,
0x00070050, 0x0000001c, 0x00000031, 0x0000002f, 0x00000030, 0x0000000d,
0x0000000f, 0x00050091, 0x0000001c, 0x00000032, 0x0000002d, 0x00000031,
0x00050041, 0x00000033, 0x00000034, 0x00000021, 0x00000022, 0x0003003e,
0x00000034, 0x00000032, 0x00060041, 0x00000036, 0x00000037, 0x00000021,
0x00000022, 0x00000035, 0x0003003e, 0x00000037, 0x0000000d, 0x0004003d,
0x0000001c, 0x0000003b, 0x0000003a, 0x0003003e, 0x00000038, 0x0000003b,
0x0004003d, 0x0000001c, 0x0000003f, 0x0000003e, 0x0007004f, 0x00000007,
0x00000040, 0x0000003f, 0x0000003f, 0x00000000, 0x00000001, 0x0004003d,
0x0000001c, 0x00000041, 0x0000003e, 0x0007004f, 0x00000007, 0x00000042,
0x00000041, 0x00000041, 0x00000002, 0x00000003, 0x0004003d, 0x00000007,
0x00000043, 0x00000009, 0x0008000c, 0x00000007, 0x00000044, 0x00000001,
0x0000002e, 0x00000040, 0x00000042, 0x00000043, 0x0003003e, 0x0000003d,
0x00000044, 0x000100fd, 0x00010038,
};
} // namespace sophia::shaders
+2
View File
@@ -1,4 +1,5 @@
#include <sophia/ui/panel_registry.hpp>
#include <tracy/Tracy.hpp>
namespace sophia {
@@ -13,6 +14,7 @@ u32 PanelRegistry::visibleCount() const {
}
void PanelRegistry::update(const FrameInfo& frameInfo) {
ZoneScoped;
pool.forEach([&frameInfo](Panel& panel) {
if (panel.isVisible()) {
panel.draw(frameInfo);
+1 -1
View File
@@ -1,7 +1,7 @@
#include <sophia/ui/ui.hpp>
#include <sophia/util/assert.hpp>
#include "rendering/imgui_renderer.hpp"
#include "renderer/imgui_renderer.hpp"
namespace sophia {
+46 -27
View File
@@ -14,10 +14,11 @@ constexpr std::array<Access, 7> ALL_ACCESSES = {
Access::Present,
};
constexpr vk::AccessFlags WRITE_ACCESSES =
vk::AccessFlagBits::eColorAttachmentWrite |
vk::AccessFlagBits::eDepthStencilAttachmentWrite |
vk::AccessFlagBits::eShaderWrite | vk::AccessFlagBits::eTransferWrite;
constexpr vk::AccessFlags2 WRITE_ACCESSES =
vk::AccessFlagBits2::eColorAttachmentWrite |
vk::AccessFlagBits2::eDepthStencilAttachmentWrite |
vk::AccessFlagBits2::eShaderStorageWrite |
vk::AccessFlagBits2::eTransferWrite;
} // namespace
@@ -25,34 +26,37 @@ AccessState getAccessState(Access access) {
switch (access) {
case Access::ColorWrite:
return {vk::ImageLayout::eColorAttachmentOptimal,
vk::PipelineStageFlagBits::eColorAttachmentOutput,
vk::AccessFlagBits::eColorAttachmentRead |
vk::AccessFlagBits::eColorAttachmentWrite};
vk::PipelineStageFlagBits2::eColorAttachmentOutput,
vk::AccessFlagBits2::eColorAttachmentRead |
vk::AccessFlagBits2::eColorAttachmentWrite};
case Access::DepthWrite:
return {vk::ImageLayout::eDepthStencilAttachmentOptimal,
vk::PipelineStageFlagBits::eEarlyFragmentTests |
vk::PipelineStageFlagBits::eLateFragmentTests,
vk::AccessFlagBits::eDepthStencilAttachmentRead |
vk::AccessFlagBits::eDepthStencilAttachmentWrite};
vk::PipelineStageFlagBits2::eEarlyFragmentTests |
vk::PipelineStageFlagBits2::eLateFragmentTests,
vk::AccessFlagBits2::eDepthStencilAttachmentRead |
vk::AccessFlagBits2::eDepthStencilAttachmentWrite};
case Access::SampledRead:
return {vk::ImageLayout::eShaderReadOnlyOptimal,
vk::PipelineStageFlagBits::eFragmentShader,
vk::AccessFlagBits::eShaderRead};
vk::PipelineStageFlagBits2::eFragmentShader,
vk::AccessFlagBits2::eShaderSampledRead};
case Access::StorageReadWrite:
return {
vk::ImageLayout::eGeneral, vk::PipelineStageFlagBits::eComputeShader,
vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite};
return {vk::ImageLayout::eGeneral,
vk::PipelineStageFlagBits2::eComputeShader,
vk::AccessFlagBits2::eShaderStorageRead |
vk::AccessFlagBits2::eShaderStorageWrite};
case Access::TransferSrc:
return {vk::ImageLayout::eTransferSrcOptimal,
vk::PipelineStageFlagBits::eTransfer,
vk::AccessFlagBits::eTransferRead};
vk::PipelineStageFlagBits2::eTransfer,
vk::AccessFlagBits2::eTransferRead};
case Access::TransferDst:
return {vk::ImageLayout::eTransferDstOptimal,
vk::PipelineStageFlagBits::eTransfer,
vk::AccessFlagBits::eTransferWrite};
vk::PipelineStageFlagBits2::eTransfer,
vk::AccessFlagBits2::eTransferWrite};
case Access::Present:
// Presentation isn't a pipeline stage; the present waits on a semaphore
// instead, so the barrier into this state has nothing to block.
return {vk::ImageLayout::ePresentSrcKHR,
vk::PipelineStageFlagBits::eBottomOfPipe, vk::AccessFlags{}};
vk::PipelineStageFlagBits2::eNone, vk::AccessFlags2{}};
}
log::fatal("Unknown Access value.");
@@ -83,22 +87,37 @@ bool needsBarrier(const AccessState& from, const AccessState& to) {
return from.layout != to.layout || writesImage(from) || writesImage(to);
}
vk::ImageMemoryBarrier makeImageBarrier(vk::Image image,
vk::ImageMemoryBarrier2 makeImageBarrier(vk::Image image,
const vk::ImageSubresourceRange& range,
const AccessState& from,
const AccessState& to) {
vk::ImageMemoryBarrier barrier{};
vk::ImageMemoryBarrier2 barrier{};
barrier.srcStageMask = from.stage;
barrier.dstStageMask = to.stage;
// 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;
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;
}
void recordImageBarriers(
vk::CommandBuffer commandBuffer,
vk::ArrayProxy<const vk::ImageMemoryBarrier2> barriers) {
if (barriers.empty()) {
return;
}
vk::DependencyInfo dependency{};
dependency.imageMemoryBarrierCount = barriers.size();
dependency.pImageMemoryBarriers = barriers.data();
commandBuffer.pipelineBarrier2(dependency);
}
} // namespace sophia
+13 -5
View File
@@ -25,8 +25,8 @@ enum class Access {
*/
struct AccessState {
vk::ImageLayout layout = vk::ImageLayout::eUndefined;
vk::PipelineStageFlags stage = vk::PipelineStageFlagBits::eTopOfPipe;
vk::AccessFlags access;
vk::PipelineStageFlags2 stage = vk::PipelineStageFlagBits2::eNone;
vk::AccessFlags2 access;
};
/**
@@ -35,8 +35,8 @@ struct AccessState {
* 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;
inline constexpr vk::PipelineStageFlags2 SWAPCHAIN_ACQUIRE_STAGE =
vk::PipelineStageFlagBits2::eColorAttachmentOutput;
AccessState getAccessState(Access access);
@@ -55,9 +55,17 @@ bool writesImage(const AccessState& state);
*/
bool needsBarrier(const AccessState& from, const AccessState& to);
vk::ImageMemoryBarrier makeImageBarrier(vk::Image image,
/**
* A barrier carrying its own stage masks, for recording with
* pipelineBarrier2. Barriers flushed together don't share stages.
*/
vk::ImageMemoryBarrier2 makeImageBarrier(vk::Image image,
const vk::ImageSubresourceRange& range,
const AccessState& from,
const AccessState& to);
void recordImageBarriers(
vk::CommandBuffer commandBuffer,
vk::ArrayProxy<const vk::ImageMemoryBarrier2> barriers);
} // namespace sophia
+18 -18
View File
@@ -28,26 +28,32 @@ Buffer::Buffer(Device& device, vk::DeviceSize instanceSize, u32 instanceCount,
alignmentSize = getAlignment(instanceSize, minOffsetAlignment);
bufferSize = alignmentSize * instanceCount;
device.createBuffer(bufferSize, usageFlags, memoryPropertyFlags, buffer,
memory);
allocation);
}
Buffer::~Buffer() {
unmap();
this->device.get()->destroyBuffer(this->buffer);
this->device.get()->freeMemory(this->memory);
vmaDestroyBuffer(this->device.getAllocator(), this->buffer, this->allocation);
}
// VMA always maps the whole allocation, so `size` only exists to keep the
// interface; `offset` is applied to the returned pointer.
vk::Result Buffer::map(vk::DeviceSize size, vk::DeviceSize offset) {
SOPHIA_ASSERT(buffer && memory, "Called map on buffer before create");
SOPHIA_ASSERT(buffer && allocation, "Called map on buffer before create");
(void)size;
vk::MemoryMapFlags emptyFlag{};
return this->device.get()->mapMemory(this->memory, offset, size, emptyFlag,
&this->mapped);
void* base = nullptr;
vk::Result result = static_cast<vk::Result>(
vmaMapMemory(this->device.getAllocator(), this->allocation, &base));
if (result == vk::Result::eSuccess) {
this->mapped = static_cast<char*>(base) + offset;
}
return result;
}
void Buffer::unmap() {
if (mapped) {
this->device.get()->unmapMemory(this->memory);
vmaUnmapMemory(this->device.getAllocator(), this->allocation);
mapped = nullptr;
}
}
@@ -66,19 +72,13 @@ void Buffer::writeToBuffer(void* data, vk::DeviceSize size,
}
vk::Result Buffer::flush(vk::DeviceSize size, vk::DeviceSize offset) {
vk::MappedMemoryRange mappedRange = {};
mappedRange.memory = memory;
mappedRange.offset = offset;
mappedRange.size = size;
return this->device.get()->flushMappedMemoryRanges(1, &mappedRange);
return static_cast<vk::Result>(vmaFlushAllocation(
this->device.getAllocator(), this->allocation, offset, size));
}
vk::Result Buffer::invalidate(vk::DeviceSize size, vk::DeviceSize offset) {
vk::MappedMemoryRange mappedRange = {};
mappedRange.memory = memory;
mappedRange.offset = offset;
mappedRange.size = size;
return this->device.get()->invalidateMappedMemoryRanges(1, &mappedRange);
return static_cast<vk::Result>(vmaInvalidateAllocation(
this->device.getAllocator(), this->allocation, offset, size));
}
vk::DescriptorBufferInfo Buffer::descriptorInfo(vk::DeviceSize size,
+1 -1
View File
@@ -55,7 +55,7 @@ class Buffer {
Device& device;
void* mapped = nullptr;
vk::Buffer buffer = VK_NULL_HANDLE;
vk::DeviceMemory memory = VK_NULL_HANDLE;
VmaAllocation allocation = VK_NULL_HANDLE;
vk::DeviceSize bufferSize;
uint32_t instanceCount;
+125 -77
View File
@@ -57,6 +57,76 @@ void destroyDebugUtilsMessengerEXT(VkInstance instance,
}
}
using DeviceFeatureChain = vk::StructureChain<
vk::PhysicalDeviceFeatures2, vk::PhysicalDeviceVulkan11Features,
vk::PhysicalDeviceVulkan12Features, vk::PhysicalDeviceVulkan13Features>;
// Single source of truth for the device features Sophia requires. Calls
// fn(featureRef, name) for each one, so the same list drives both the
// suitability check and device creation.
template <typename Fn>
static void forEachRequiredFeature(DeviceFeatureChain& chain, Fn&& fn) {
auto& f10 = chain.get<vk::PhysicalDeviceFeatures2>().features;
auto& f11 = chain.get<vk::PhysicalDeviceVulkan11Features>();
auto& f12 = chain.get<vk::PhysicalDeviceVulkan12Features>();
auto& f13 = chain.get<vk::PhysicalDeviceVulkan13Features>();
#define SOPHIA_FEATURE(features, name) fn(features.name, #name)
// Indirect drawing
SOPHIA_FEATURE(f10, multiDrawIndirect);
SOPHIA_FEATURE(f10, drawIndirectFirstInstance);
SOPHIA_FEATURE(f11, shaderDrawParameters);
SOPHIA_FEATURE(f12, drawIndirectCount);
// Bindless descriptors
SOPHIA_FEATURE(f12, descriptorIndexing);
SOPHIA_FEATURE(f12, runtimeDescriptorArray);
SOPHIA_FEATURE(f12, descriptorBindingPartiallyBound);
SOPHIA_FEATURE(f12, descriptorBindingVariableDescriptorCount);
SOPHIA_FEATURE(f12, descriptorBindingUpdateUnusedWhilePending);
SOPHIA_FEATURE(f12, descriptorBindingSampledImageUpdateAfterBind);
SOPHIA_FEATURE(f12, descriptorBindingStorageImageUpdateAfterBind);
SOPHIA_FEATURE(f12, descriptorBindingStorageBufferUpdateAfterBind);
SOPHIA_FEATURE(f12, shaderSampledImageArrayNonUniformIndexing);
SOPHIA_FEATURE(f12, shaderStorageImageArrayNonUniformIndexing);
SOPHIA_FEATURE(f12, shaderStorageBufferArrayNonUniformIndexing);
// Buffer pointers
SOPHIA_FEATURE(f12, bufferDeviceAddress);
SOPHIA_FEATURE(f12, scalarBlockLayout);
// Synchronization
SOPHIA_FEATURE(f12, timelineSemaphore);
SOPHIA_FEATURE(f13, synchronization2);
// Rendering
SOPHIA_FEATURE(f13, dynamicRendering);
#undef SOPHIA_FEATURE
}
static bool checkDeviceFeatureSupport(const vk::PhysicalDevice& device) {
vk::PhysicalDeviceProperties properties = device.getProperties();
if (properties.apiVersion < SOPHIA_VULKAN_API_VERSION) {
log::verbose("Skipping ", properties.deviceName,
": Vulkan 1.3 not supported");
return false;
}
auto supported = device.getFeatures2<
vk::PhysicalDeviceFeatures2, vk::PhysicalDeviceVulkan11Features,
vk::PhysicalDeviceVulkan12Features, vk::PhysicalDeviceVulkan13Features>();
bool allSupported = true;
forEachRequiredFeature(supported, [&](vk::Bool32 has, const char* name) {
if (!has) {
log::verbose("Skipping ", properties.deviceName,
": missing device feature ", name);
allSupported = false;
}
});
return allSupported;
}
Device::Device(Window* window)
: headless{window == nullptr},
enabledExtensions{headless ? std::vector<const char*>{}
@@ -69,10 +139,14 @@ Device::Device(Window* window)
}
pickPhysicalDevice();
createLogicalDevice();
createAllocator();
createCommandPool();
}
Device::~Device() {
vmaDestroyAllocator(this->allocator);
log::trace("destroyed VmaAllocator");
if (this->enableValidationLayers) {
destroyDebugUtilsMessengerEXT(this->instance.get(), this->debugMessenger,
nullptr);
@@ -88,22 +162,6 @@ Device::~Device() {
}
}
u32 Device::findMemoryType(u32 typeFilter, vk::MemoryPropertyFlags properties) {
vk::PhysicalDeviceMemoryProperties memoryProperties =
this->physicalDevice.getMemoryProperties();
for (u32 i = 0; i < memoryProperties.memoryTypeCount; i++) {
if ((typeFilter & (1 << i)) &&
(memoryProperties.memoryTypes[i].propertyFlags & properties) ==
properties) {
return i;
}
}
log::fatal("failed to find suitable memory type");
throw std::runtime_error("failed to find suitable memory type");
}
vk::Format Device::findSupportedFormat(
const std::vector<vk::Format>& candidates, vk::ImageTiling tiling,
vk::FormatFeatureFlags features) {
@@ -125,35 +183,26 @@ vk::Format Device::findSupportedFormat(
void Device::createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage,
vk::MemoryPropertyFlags properties,
vk::Buffer& buffer, vk::DeviceMemory& bufferMemory) {
vk::Buffer& buffer, VmaAllocation& allocation) {
vk::BufferCreateInfo bufferInfo = {};
bufferInfo.size = size;
bufferInfo.usage = usage;
bufferInfo.sharingMode = vk::SharingMode::eExclusive;
try {
buffer = this->device->createBuffer(bufferInfo);
} catch (const vk::SystemError& error) {
log::fatal("failed to create vertex buffer");
throw std::runtime_error("failed to create vertex buffer");
VmaAllocationCreateInfo allocInfo = {};
allocInfo.requiredFlags = static_cast<VkMemoryPropertyFlags>(properties);
VkBuffer rawBuffer = VK_NULL_HANDLE;
VkResult result = vmaCreateBuffer(
this->allocator, &static_cast<const VkBufferCreateInfo&>(bufferInfo),
&allocInfo, &rawBuffer, &allocation, nullptr);
if (result != VK_SUCCESS) {
log::fatal("failed to create buffer. Error: ",
vk::to_string(static_cast<vk::Result>(result)));
throw std::runtime_error("failed to create buffer");
}
vk::MemoryRequirements memoryRequirements =
this->device->getBufferMemoryRequirements(buffer);
vk::MemoryAllocateInfo allocInfo = {};
allocInfo.allocationSize = memoryRequirements.size;
allocInfo.memoryTypeIndex =
findMemoryType(memoryRequirements.memoryTypeBits, properties);
try {
bufferMemory = this->device->allocateMemory(allocInfo);
} catch (const vk::SystemError& error) {
log::fatal("failed to allocate vertex buffer memory");
throw std::runtime_error("failed to allocate vertex buffer memory");
}
this->device->bindBufferMemory(buffer, bufferMemory, 0);
buffer = rawBuffer;
}
vk::CommandBuffer Device::beginSingleTimeCommands() {
@@ -220,43 +269,21 @@ void Device::copyBuffer(vk::Buffer sourceBuffer, vk::Buffer destinationBuffer,
void Device::createImageWithInfo(const vk::ImageCreateInfo& imageInfo,
vk::MemoryPropertyFlags properties,
vk::Image& image,
vk::DeviceMemory& imageMemory) {
try {
vk::Result result = this->device->createImage(&imageInfo, nullptr, &image);
if (result != vk::Result::eSuccess) {
throw vk::SystemError(result);
}
} catch (const vk::SystemError& error) {
log::fatal("failed to create image. Error: ", error.what());
vk::Image& image, VmaAllocation& allocation) {
VmaAllocationCreateInfo allocInfo = {};
allocInfo.requiredFlags = static_cast<VkMemoryPropertyFlags>(properties);
VkImage rawImage = VK_NULL_HANDLE;
VkResult result = vmaCreateImage(
this->allocator, &static_cast<const VkImageCreateInfo&>(imageInfo),
&allocInfo, &rawImage, &allocation, nullptr);
if (result != VK_SUCCESS) {
log::fatal("failed to create image. Error: ",
vk::to_string(static_cast<vk::Result>(result)));
throw std::runtime_error("failed to create image");
}
vk::MemoryRequirements memoryRequirements =
this->device->getImageMemoryRequirements(image);
vk::MemoryAllocateInfo allocateInfo{};
allocateInfo.allocationSize = memoryRequirements.size;
allocateInfo.memoryTypeIndex =
findMemoryType(memoryRequirements.memoryTypeBits, properties);
try {
vk::Result result =
this->device->allocateMemory(&allocateInfo, nullptr, &imageMemory);
if (result != vk::Result::eSuccess) {
throw vk::SystemError(result);
}
} catch (const vk::SystemError& error) {
log::fatal("failed to allocate image memory. Error: ", error.what());
throw std::runtime_error("failed to allocate image memory");
}
try {
this->device->bindImageMemory(image, imageMemory, 0);
} catch (const vk::SystemError& error) {
log::fatal("failed to bind image memory. Error: ", error.what());
throw std::runtime_error("failed to bind image memory");
}
image = rawImage;
}
void Device::populateImGuiInitInfo(ImGui_ImplVulkan_InitInfo& initInfo) {
@@ -429,6 +456,10 @@ bool Device::isPhysicalDeviceSuitable(const vk::PhysicalDevice& device) {
bool extensionsSupported = checkDeviceExtensionSupport(device);
if (!checkDeviceFeatureSupport(device)) {
return false;
}
if (this->headless) {
return indices.graphicsFamily.has_value() && extensionsSupported;
}
@@ -486,15 +517,17 @@ void Device::createLogicalDevice() {
{vk::DeviceQueueCreateFlags(), queueFamily, 1, &queuePriority});
}
vk::PhysicalDeviceVulkan13Features vulkan13Features{};
vulkan13Features.dynamicRendering = vk::True;
DeviceFeatureChain enabledFeatures;
forEachRequiredFeature(enabledFeatures, [](vk::Bool32& feature, const char*) {
feature = vk::True;
});
auto deviceFeatures = vk::PhysicalDeviceFeatures();
auto createInfo = vk::DeviceCreateInfo(
vk::DeviceCreateFlags(), static_cast<uint32_t>(queueCreateInfos.size()),
queueCreateInfos.data());
createInfo.pEnabledFeatures = &deviceFeatures;
createInfo.pNext = &vulkan13Features;
// Core features are passed through PhysicalDeviceFeatures2 in the pNext
// chain, so pEnabledFeatures must stay null.
createInfo.pNext = &enabledFeatures.get<vk::PhysicalDeviceFeatures2>();
createInfo.enabledExtensionCount =
static_cast<uint32_t>(this->enabledExtensions.size());
@@ -514,6 +547,21 @@ void Device::createLogicalDevice() {
}
}
void Device::createAllocator() {
VmaAllocatorCreateInfo createInfo = {};
createInfo.flags = VMA_ALLOCATOR_CREATE_BUFFER_DEVICE_ADDRESS_BIT;
createInfo.vulkanApiVersion = SOPHIA_VULKAN_API_VERSION;
createInfo.instance = this->instance.get();
createInfo.physicalDevice = this->physicalDevice;
createInfo.device = this->device.get();
if (vmaCreateAllocator(&createInfo, &this->allocator) != VK_SUCCESS) {
log::fatal("failed to create VmaAllocator");
throw std::runtime_error("failed to create VmaAllocator");
}
log::trace("created VmaAllocator");
}
SwapchainSupportDetails Device::querySwapchainSupport(
vk::PhysicalDevice device) {
SwapchainSupportDetails details;
+7 -4
View File
@@ -1,6 +1,7 @@
#pragma once
#include <imgui_impl_vulkan.h>
#include <vk_mem_alloc.h>
#include <memory>
#include <optional>
@@ -47,6 +48,7 @@ class Device {
void waitIdle() { this->device->waitIdle(); }
VmaAllocator getAllocator() const { return this->allocator; }
vk::CommandPool getCommandPool() const { return this->commandPool; }
vk::SurfaceKHR getSurface() const { return surface; }
vk::Queue getGraphicsQueue() const { return graphicsQueue; }
@@ -60,15 +62,13 @@ class Device {
return querySwapchainSupport(this->physicalDevice);
}
u32 findMemoryType(u32 typeFilter, vk::MemoryPropertyFlags properties);
vk::Format findSupportedFormat(const std::vector<vk::Format>& candidates,
vk::ImageTiling tiling,
vk::FormatFeatureFlags features);
void createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage,
vk::MemoryPropertyFlags properties, vk::Buffer& buffer,
vk::DeviceMemory& bufferMemory);
VmaAllocation& allocation);
vk::CommandBuffer beginSingleTimeCommands();
@@ -79,7 +79,7 @@ class Device {
void createImageWithInfo(const vk::ImageCreateInfo& imageInfo,
vk::MemoryPropertyFlags properties, vk::Image& image,
vk::DeviceMemory& imageMemory);
VmaAllocation& allocation);
void populateImGuiInitInfo(ImGui_ImplVulkan_InitInfo& initInfo);
@@ -100,6 +100,7 @@ class Device {
SwapchainSupportDetails querySwapchainSupport(vk::PhysicalDevice device);
void createLogicalDevice();
void createAllocator();
void createCommandPool();
vk::UniqueInstance instance;
@@ -111,6 +112,8 @@ class Device {
vk::PhysicalDevice physicalDevice = VK_NULL_HANDLE;
vk::UniqueDevice device;
VmaAllocator allocator = VK_NULL_HANDLE;
vk::Queue graphicsQueue;
vk::Queue presentQueue;
+36 -23
View File
@@ -3,11 +3,35 @@
#include <cstring>
#include <sophia/util/logger.hpp>
#include <stdexcept>
#include <utility>
#include "vulkan/device.hpp"
namespace sophia {
Image::Image(Image&& other) noexcept
: device{other.device},
externalImage{other.externalImage},
image{std::exchange(other.image, nullptr)},
allocation{std::exchange(other.allocation, VK_NULL_HANDLE)},
view{std::move(other.view)},
sampler{std::move(other.sampler)},
format{other.format},
extent{other.extent},
aspectMask{other.aspectMask},
mipLevels{other.mipLevels},
arrayLayers{other.arrayLayers},
layout{other.layout} {}
Image::~Image() {
this->sampler.reset();
this->view.reset();
if (this->image) {
vmaDestroyImage(this->device.getAllocator(), this->image, this->allocation);
}
}
vk::ImageSubresourceRange Image::getSubresourceRange() const {
return vk::ImageSubresourceRange{this->aspectMask, 0, this->mipLevels, 0,
this->arrayLayers};
@@ -32,10 +56,9 @@ void Image::transitionLayout(vk::CommandBuffer commandBuffer,
AccessState to = getStateForLayout(newLayout);
if (needsBarrier(from, to)) {
vk::ImageMemoryBarrier barrier =
vk::ImageMemoryBarrier2 barrier =
makeImageBarrier(this->get(), this->getSubresourceRange(), from, to);
commandBuffer.pipelineBarrier(from.stage, to.stage, {}, nullptr, nullptr,
barrier);
recordImageBarriers(commandBuffer, barrier);
}
this->layout = newLayout;
@@ -194,35 +217,26 @@ std::unique_ptr<Image> Image::Builder::build() {
imageInfo.sharingMode = vk::SharingMode::eExclusive;
imageInfo.samples = this->samples;
result->image = this->device.get()->createImageUnique(imageInfo);
vk::MemoryRequirements memoryRequirements =
this->device.get()->getImageMemoryRequirements(result->image.get());
vk::MemoryAllocateInfo allocInfo{};
allocInfo.allocationSize = memoryRequirements.size;
allocInfo.memoryTypeIndex = this->device.findMemoryType(
memoryRequirements.memoryTypeBits, this->memoryProperties);
result->memory = this->device.get()->allocateMemoryUnique(allocInfo);
this->device.get()->bindImageMemory(result->image.get(),
result->memory.get(), 0);
this->device.createImageWithInfo(imageInfo, this->memoryProperties,
result->image, result->allocation);
}
if (!this->isExternalImage && this->initialData != nullptr) {
VmaAllocator allocator = this->device.getAllocator();
vk::Buffer stagingBuffer;
vk::DeviceMemory stagingMemory;
VmaAllocation stagingAllocation;
this->device.createBuffer(this->initialDataSize,
vk::BufferUsageFlagBits::eTransferSrc,
vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent,
stagingBuffer, stagingMemory);
stagingBuffer, stagingAllocation);
void* mapped =
this->device.get()->mapMemory(stagingMemory, 0, this->initialDataSize);
void* mapped = nullptr;
vmaMapMemory(allocator, stagingAllocation, &mapped);
memcpy(mapped, this->initialData,
static_cast<size_t>(this->initialDataSize));
this->device.get()->unmapMemory(stagingMemory);
vmaUnmapMemory(allocator, stagingAllocation);
vk::BufferImageCopy region{};
region.bufferOffset = 0;
@@ -247,8 +261,7 @@ std::unique_ptr<Image> Image::Builder::build() {
}
this->device.endSingleTimeCommands(commandBuffer);
this->device.get()->destroyBuffer(stagingBuffer);
this->device.get()->freeMemory(stagingMemory);
vmaDestroyBuffer(allocator, stagingBuffer, stagingAllocation);
}
if (this->createView) {
+7 -4
View File
@@ -1,5 +1,7 @@
#pragma once
#include <vk_mem_alloc.h>
#include <memory>
#include <sophia/types.hpp>
#include <vulkan/vulkan.hpp>
@@ -16,11 +18,12 @@ class Image {
Image(const Image&) = delete;
Image& operator=(const Image&) = delete;
Image(Image&&) = default;
Image(Image&& other) noexcept;
Image& operator=(Image&&) = delete;
~Image();
vk::Image get() const {
return this->image ? this->image.get() : this->externalImage;
return this->image ? this->image : this->externalImage;
}
vk::ImageView getView() const { return this->view.get(); }
vk::Sampler getSampler() const { return this->sampler.get(); }
@@ -56,8 +59,8 @@ class Image {
Device& device;
vk::Image externalImage;
vk::UniqueImage image;
vk::UniqueDeviceMemory memory;
vk::Image image;
VmaAllocation allocation = VK_NULL_HANDLE;
vk::UniqueImageView view;
vk::UniqueSampler sampler;
@@ -95,6 +95,12 @@ GraphicsPipeline::Builder& GraphicsPipeline::Builder::setVertexInput(
return *this;
}
GraphicsPipeline::Builder& GraphicsPipeline::Builder::setAlphaBlending(
bool enabled) {
this->alphaBlending = enabled;
return *this;
}
std::unique_ptr<GraphicsPipeline> GraphicsPipeline::Builder::build() const {
if (this->vertexShader == nullptr || this->fragmentShader == nullptr) {
log::fatal(
@@ -128,7 +134,8 @@ std::unique_ptr<GraphicsPipeline> GraphicsPipeline::Builder::build() const {
this->device, *this->vertexShader, *this->fragmentShader,
this->pipelineLayout, this->colorAttachmentFormats,
this->depthAttachmentFormat, this->stencilAttachmentFormat,
this->bindingDescriptions, this->attributeDescriptions));
this->bindingDescriptions, this->attributeDescriptions,
this->alphaBlending));
}
GraphicsPipeline::GraphicsPipeline(
@@ -137,10 +144,21 @@ GraphicsPipeline::GraphicsPipeline(
std::vector<vk::Format> colorAttachmentFormats,
vk::Format depthAttachmentFormat, vk::Format stencilAttachmentFormat,
std::vector<vk::VertexInputBindingDescription> bindingDescriptions,
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions)
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions,
bool alphaBlending)
: Pipeline(device, vk::PipelineBindPoint::eGraphics) {
setDefaultGraphicsPipelineConfig();
if (alphaBlending) {
vk::PipelineColorBlendAttachmentState& blend =
this->config.colorBlendAttachment;
blend.blendEnable = vk::True;
blend.srcColorBlendFactor = vk::BlendFactor::eSrcAlpha;
blend.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha;
blend.srcAlphaBlendFactor = vk::BlendFactor::eOne;
blend.dstAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha;
}
this->config.bindingDescriptions = std::move(bindingDescriptions);
this->config.attributeDescriptions = std::move(attributeDescriptions);
this->config.pipelineLayout = pipelineLayout;
@@ -47,7 +47,8 @@ class GraphicsPipeline : public Pipeline {
std::vector<vk::Format> colorAttachmentFormats,
vk::Format depthAttachmentFormat, vk::Format stencilAttachmentFormat,
std::vector<vk::VertexInputBindingDescription> bindingDescriptions,
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions);
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions,
bool alphaBlending);
void setDefaultGraphicsPipelineConfig();
@@ -69,6 +70,8 @@ class GraphicsPipeline::Builder {
Builder& setVertexInput(
std::vector<vk::VertexInputBindingDescription> bindingDescriptions,
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions);
// Straight (non-premultiplied) "over" blending on every color attachment.
Builder& setAlphaBlending(bool enabled = true);
std::unique_ptr<GraphicsPipeline> build() const;
@@ -83,6 +86,7 @@ class GraphicsPipeline::Builder {
vk::Format stencilAttachmentFormat = vk::Format::eUndefined;
std::vector<vk::VertexInputBindingDescription> bindingDescriptions{};
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions{};
bool alphaBlending = false;
};
} // namespace sophia
+2
View File
@@ -0,0 +1,2 @@
#define VMA_IMPLEMENTATION
#include <vk_mem_alloc.h>
+95 -20
View File
@@ -1,3 +1,5 @@
#include <imgui.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
@@ -7,8 +9,12 @@
#include <iostream>
#include <random>
#include <sophia/application.hpp>
#include <sophia/components.hpp>
#include <sophia/entity.hpp>
#include <sophia/event.hpp>
#include <sophia/key_event.hpp>
#include <sophia/scene_view.hpp>
#include <sophia/ui/panel.hpp>
#include <stdexcept>
#include <string>
#include <vector>
@@ -16,6 +22,24 @@
struct Boid {
glm::vec2 position;
glm::vec2 velocity;
sophia::Entity entity;
};
// Shows a render target filling the whole panel.
class ViewportPanel : public sophia::Panel {
public:
ViewportPanel(sophia::UI& ui, sophia::RenderTargetHandle target)
: Panel{{.name = "Boids",
.windowFlags = ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse}},
ui{ui},
target{target} {}
void onUpdate(const sophia::FrameInfo&) override { ui.image(target); }
private:
sophia::UI& ui;
sophia::RenderTargetHandle target;
};
constexpr uint32_t DEFAULT_NUMBER_OF_BOIDS = 25;
@@ -63,6 +87,8 @@ class Boids2D : public sophia::Application {
[this](sophia::KeyReleasedEvent& e) {
setScrollKeyHeld(e.getKeyCode(), false);
});
debugUI().show();
}
void onCreate() override {
@@ -81,28 +107,51 @@ class Boids2D : public sophia::Application {
boid.velocity = glm::vec2{std::cos(theta), std::sin(theta)} * MAX_SPEED;
}
// Boid shape in local space, nose pointing along +x; each boid is drawn
// as an instance of this one mesh with a per-boid transform.
std::vector<sophia::CanvasVertex> vertices{
{{BOID_LENGTH * 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}},
{{-BOID_LENGTH * 0.5f, BOID_WIDTH * 0.5f}, {1.0f, 1.0f, 1.0f}},
{{-BOID_LENGTH * 0.5f, -BOID_WIDTH * 0.5f}, {1.0f, 1.0f, 1.0f}},
};
boidMesh = resources().create<sophia::Mesh>(std::move(vertices));
boidTexture = createBoidTexture();
sceneHandle = scenes().create();
sophia::Scene& scene = scenes().get(sceneHandle);
cameraEntity = scene.createCamera();
std::uniform_real_distribution<float> tint{0.0f, 1.0f};
for (Boid& boid : boids) {
boid.entity = scene.createEntity("Boid");
boid.entity.addComponent<sophia::SpriteComponent>(sophia::SpriteComponent{
.texture = boidTexture,
.color = glm::vec4{glm::mix(glm::vec3{0.55f, 0.8f, 1.0f},
glm::vec3{1.0f, 0.85f, 0.55f}, tint(rng)),
1.0f},
.size = {BOID_LENGTH, BOID_WIDTH}});
}
target = resources().create<sophia::RenderTarget>();
sceneViews().create({.scene = sceneHandle,
.target = target,
.clearColor = {0.01f, 0.01f, 0.02f, 1.0f}});
panels().emplace<ViewportPanel>(ui(), target);
panels().setDefaultLayout({.center = {"Boids"}});
}
void onUpdate(const sophia::FrameInfo& frameInfo) override {
float scrollDistance =
SCROLL_SPEED * static_cast<float>(frameInfo.deltaTime);
if (scrollLeft) camera.position.x -= scrollDistance;
if (scrollRight) camera.position.x += scrollDistance;
if (scrollUp) camera.position.y -= scrollDistance;
if (scrollDown) camera.position.y += scrollDistance;
glm::vec3& cameraPosition =
cameraEntity.getComponent<sophia::TransformComponent>().position;
if (scrollLeft) cameraPosition.x -= scrollDistance;
if (scrollRight) cameraPosition.x += scrollDistance;
if (scrollUp) cameraPosition.y -= scrollDistance;
if (scrollDown) cameraPosition.y += scrollDistance;
// One world unit per pixel, whatever size the viewport is.
cameraEntity.getComponent<sophia::CameraComponent>().ortho.height =
resources().getExtent(target).y;
updateBoids(static_cast<float>(frameInfo.deltaTime));
for (const Boid& boid : boids) {
drawBoid(boid);
for (Boid& boid : boids) {
syncTransform(boid);
}
}
@@ -212,17 +261,40 @@ class Boids2D : public sophia::Application {
if (position.y < -WORLD_HALF_HEIGHT) position.y = WORLD_HALF_HEIGHT;
}
void drawBoid(const Boid& boid) {
void syncTransform(Boid& boid) {
glm::vec2 direction = boid.velocity;
float speed = glm::length(direction);
direction = speed > 1e-5f ? direction / speed : glm::vec2{0.0f, -1.0f};
float angle = std::atan2(direction.y, direction.x);
glm::mat4 transform =
glm::translate(glm::mat4{1.f}, glm::vec3(boid.position, 0.0f)) *
glm::rotate(glm::mat4{1.f}, angle, glm::vec3(0.0f, 0.0f, 1.0f));
auto& transform = boid.entity.getComponent<sophia::TransformComponent>();
transform.position = glm::vec3(boid.position, 0.0f);
transform.rotation = glm::angleAxis(angle, glm::vec3(0.0f, 0.0f, 1.0f));
}
drawMesh(boidMesh, transform);
// Boid shape, nose pointing along +u, as an anti-aliased white triangle on
// a transparent background; each boid's sprite tints it.
sophia::TextureHandle createBoidTexture() {
constexpr uint32_t WIDTH = 64;
constexpr uint32_t HEIGHT = 40;
std::vector<uint8_t> pixels(WIDTH * HEIGHT * 4);
for (uint32_t y = 0; y < HEIGHT; ++y) {
for (uint32_t x = 0; x < WIDTH; ++x) {
float u = (static_cast<float>(x) + 0.5f) / WIDTH;
float v = (static_cast<float>(y) + 0.5f) / HEIGHT;
// Distance inside the triangle's edge, in pixels.
float inside = (0.5f * (1.0f - u) - std::abs(v - 0.5f)) * HEIGHT;
float alpha = std::clamp(inside + 0.5f, 0.0f, 1.0f);
uint8_t* pixel = &pixels[(y * WIDTH + x) * 4];
pixel[0] = pixel[1] = pixel[2] = 255;
pixel[3] = static_cast<uint8_t>(alpha * 255.0f);
}
}
return resources().create<sophia::Texture>(WIDTH, HEIGHT,
std::move(pixels));
}
void setScrollKeyHeld(sophia::KeyCode keycode, bool held) {
@@ -235,7 +307,10 @@ class Boids2D : public sophia::Application {
std::vector<Boid> boids;
std::vector<std::vector<uint32_t>> grid{
static_cast<size_t>(GRID_WIDTH * GRID_HEIGHT)};
sophia::MeshHandle boidMesh{};
sophia::TextureHandle boidTexture{};
sophia::SceneHandle sceneHandle{};
sophia::RenderTargetHandle target{};
sophia::Entity cameraEntity;
bool scrollLeft = false;
bool scrollRight = false;
bool scrollUp = false;
+7
View File
@@ -0,0 +1,7 @@
add_executable(image_viewer main.cpp)
target_link_libraries(image_viewer PRIVATE ${PROJECT_NAME})
target_compile_definitions(image_viewer PRIVATE
IMAGE_VIEWER_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

+77
View File
@@ -0,0 +1,77 @@
#include <imgui.h>
#include <algorithm>
#include <sophia/application.hpp>
#include <sophia/components.hpp>
#include <sophia/entity.hpp>
#include <sophia/scene_view.hpp>
#include <sophia/ui/panel.hpp>
#include <string>
// Shows a render target filling the whole panel.
class ViewportPanel : public sophia::Panel {
public:
ViewportPanel(sophia::UI& ui, sophia::RenderTargetHandle target)
: Panel{{.name = "Image",
.windowFlags = ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollWithMouse}},
ui{ui},
target{target} {}
void onUpdate(const sophia::FrameInfo&) override { ui.image(target); }
private:
sophia::UI& ui;
sophia::RenderTargetHandle target;
};
// Displays an image file (anything stb_image reads: JPEG, PNG, BMP, ...) as a
// sprite in a 2D scene, scaled to fit the viewport.
// image_viewer [path]
class ImageViewer : public sophia::Application {
public:
ImageViewer()
: Application{{.width = 1000, .height = 750, .name = "ImageViewer"}} {}
void onCreate() override {
std::string path =
args.size() > 1 ? args[1] : IMAGE_VIEWER_ASSET_DIR "/dog.jpeg";
sophia::SceneHandle sceneHandle = scenes().create();
sophia::Scene& scene = scenes().get(sceneHandle);
camera = scene.createCamera();
sophia::TextureHandle texture = resources().load<sophia::Texture>(path);
imageSize = resources().getExtent(texture);
// One world unit per image pixel; the camera zooms to fit.
sophia::Entity image = scene.createEntity("Image");
image.addComponent<sophia::SpriteComponent>(
sophia::SpriteComponent{.texture = texture, .size = imageSize});
target = resources().create<sophia::RenderTarget>();
sceneViews().create({.scene = sceneHandle,
.target = target,
.clearColor = {0.1f, 0.1f, 0.15f, 1.f}});
panels().emplace<ViewportPanel>(ui(), target);
// panels().setDefaultLayout({.center = {"Image"}});
}
void onUpdate(const sophia::FrameInfo&) override {
// Fit the whole image: show enough height that neither dimension of the
// image overflows the viewport.
glm::vec2 viewport = resources().getExtent(target);
float viewportAspect = viewport.x / viewport.y;
camera.getComponent<sophia::CameraComponent>().ortho.height =
std::max(imageSize.y, imageSize.x / viewportAspect);
}
private:
sophia::Entity camera;
sophia::RenderTargetHandle target{};
glm::vec2 imageSize{1.f};
};
SOPHIA_MAIN(ImageViewer)
Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

+3 -2
View File
@@ -27,11 +27,12 @@ class Testbed : public sophia::Application {
Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {}
void onCreate() override {
view = views().create({.scene = scenes().create(),
view = sceneViews().create(
{.scene = scenes().create(),
.target = resources().create<sophia::RenderTarget>(),
.clearColor = {0.1f, 0.1f, 0.15f, 1.f}});
panels().emplace<Viewport>(ui(), views().get(view).target);
panels().emplace<Viewport>(ui(), sceneViews().get(view).target);
}
private:
+17
View File
@@ -17,6 +17,11 @@ target_link_libraries(vulkan INTERFACE
Vulkan::shaderc_combined
)
# 2. VMA (header-only, ships with the Vulkan SDK)
add_library(vma INTERFACE)
target_include_directories(vma INTERFACE ${Vulkan_INCLUDE_DIRS}/vma)
target_link_libraries(vma INTERFACE vulkan)
# 3. GLFW (static)
option(GLFW_BUILD_EXAMPLES "Build the GLFW example programs" OFF)
option(GLFW_BUILD_TESTS "Build the GLFW test programs" OFF)
@@ -34,6 +39,18 @@ target_include_directories(glm INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/glm)
add_library(entt INTERFACE)
target_include_directories(entt INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/entt)
# stb_image (static)
add_library(stb_image STATIC
${CMAKE_CURRENT_SOURCE_DIR}/stb_image/stb_image.cpp
)
target_include_directories(stb_image PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/stb_image)
# Tracy profiler client (static). Configure with -DTRACY_ENABLE=OFF to compile
# all profiling macros out.
option(TRACY_ENABLE "Enable Tracy profiling" ON)
option(TRACY_ON_DEMAND "Only collect data while the viewer is connected" ON)
add_subdirectory(tracy)
# 5. IMGUI (static)
set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/imgui)
+2
View File
@@ -0,0 +1,2 @@
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
+7988
View File
File diff suppressed because it is too large Load Diff
Vendored Submodule
+1
Submodule third_party/tracy added at 30997d5ca6