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:
@@ -14,5 +14,4 @@ TODO
|
|||||||
|
|
||||||
## Testing Instructions
|
## Testing Instructions
|
||||||
|
|
||||||
1. Build engine via `bin/dev build && cd build/ && make -j4`
|
1. Build engine and run editor via `bin/dev editor`
|
||||||
2. Run testbed via `./testbed`
|
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ pids
|
|||||||
*.pid.lock
|
*.pid.lock
|
||||||
imgui.ini
|
imgui.ini
|
||||||
|
|
||||||
|
# Python bytecode (bin/dev and bin/dev_commands)
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
# Tracy captures
|
||||||
|
traces/
|
||||||
|
*.tracy
|
||||||
|
|
||||||
# dotenv environment variables file
|
# dotenv environment variables file
|
||||||
.envrc
|
.envrc
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -8,3 +8,6 @@
|
|||||||
path = third_party/imgui
|
path = third_party/imgui
|
||||||
url = https://github.com/ocornut/imgui.git
|
url = https://github.com/ocornut/imgui.git
|
||||||
branch = docking
|
branch = docking
|
||||||
|
[submodule "third_party/tracy"]
|
||||||
|
path = third_party/tracy
|
||||||
|
url = https://github.com/wolfpld/tracy.git
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# sophia
|
# sophia
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
COMING SOON
|
COMING SOON
|
||||||
|
|||||||
@@ -2,13 +2,16 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import struct
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
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 = {
|
COMMAND_TO_METHOD = {
|
||||||
"branch-from": "branch_from",
|
"branch-from": "branch_from",
|
||||||
"bash-completions": "bash_completions",
|
"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"
|
WAIT_FOR_PROCESS = "wait_for_process"
|
||||||
|
|
||||||
class DevHelper:
|
class DevCommand(GiteaCommand, RenderDocCommand, ShadersCommand, TracyCommand):
|
||||||
SHADER_EXTENSIONS = (".vert", ".frag", ".comp", ".geom", ".tesc", ".tese")
|
|
||||||
PROJECT_NAME = "sophia"
|
PROJECT_NAME = "sophia"
|
||||||
DEFAULT_PRESET = "debug"
|
DEFAULT_PRESET = "debug"
|
||||||
|
|
||||||
@@ -50,6 +52,20 @@ class DevHelper:
|
|||||||
preset, fresh = self._parse_preset_args(args, "build")
|
preset, fresh = self._parse_preset_args(args, "build")
|
||||||
self._build_preset(preset, fresh)
|
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
|
# Builds the debug preset (incrementally), then runs the testbed from
|
||||||
# build/debug. Any arguments are passed through to the testbed.
|
# build/debug. Any arguments are passed through to the testbed.
|
||||||
@@ -64,87 +80,6 @@ class DevHelper:
|
|||||||
os.chdir(preset_dir)
|
os.chdir(preset_dir)
|
||||||
os.execv(binary, [binary, *args])
|
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
|
# Custom helpers
|
||||||
|
|
||||||
def ownit(self, *args, **kwargs):
|
def ownit(self, *args, **kwargs):
|
||||||
@@ -166,8 +101,8 @@ class DevHelper:
|
|||||||
def _commands(self):
|
def _commands(self):
|
||||||
return sorted(
|
return sorted(
|
||||||
METHOD_TO_COMMAND.get(name, name)
|
METHOD_TO_COMMAND.get(name, name)
|
||||||
for name in vars(DevHelper)
|
for name in dir(DevCommand)
|
||||||
if not name.startswith("_") and name != "call" and callable(getattr(DevHelper, name))
|
if not name.startswith("_") and name != "call" and callable(getattr(DevCommand, name))
|
||||||
)
|
)
|
||||||
|
|
||||||
def _parse_preset_args(self, args, command):
|
def _parse_preset_args(self, args, command):
|
||||||
@@ -216,166 +151,6 @@ class DevHelper:
|
|||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
sys.exit(result.returncode)
|
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):
|
def _wait_for_process_with_logging(self, command):
|
||||||
process = self._exec(command, capture=True)
|
process = self._exec(command, capture=True)
|
||||||
for line in process.stdout:
|
for line in process.stdout:
|
||||||
@@ -419,4 +194,4 @@ class DevHelper:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
DevHelper().call(*sys.argv[1:])
|
DevCommand().call(*sys.argv[1:])
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)")
|
||||||
@@ -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
@@ -1,5 +1,7 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <sophia/application.hpp>
|
#include <sophia/application.hpp>
|
||||||
|
#include <sophia/components.hpp>
|
||||||
|
#include <sophia/entity.hpp>
|
||||||
|
|
||||||
#include "panels/asset_browser_panel.hpp"
|
#include "panels/asset_browser_panel.hpp"
|
||||||
#include "panels/inspector_panel.hpp"
|
#include "panels/inspector_panel.hpp"
|
||||||
@@ -15,13 +17,19 @@ class Editor : public sophia::Application {
|
|||||||
|
|
||||||
debugUI().show();
|
debugUI().show();
|
||||||
|
|
||||||
panels().emplace<SceneHierarchyPanel>();
|
textures.emplace_back("Checker", createCheckerTexture());
|
||||||
panels().emplace<InspectorPanel>();
|
|
||||||
|
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>();
|
panels().emplace<AssetBrowserPanel>();
|
||||||
sophia::RenderTargetHandle sceneTarget =
|
sophia::RenderTargetHandle sceneTarget =
|
||||||
resources().create<sophia::RenderTarget>();
|
resources().create<sophia::RenderTarget>();
|
||||||
views().create({.scene = scenes().create(), .target = sceneTarget});
|
sceneViews().create({.scene = sceneHandle, .target = sceneTarget});
|
||||||
panels().emplace<ViewportPanel>(ui(), sceneTarget);
|
panels().emplace<ViewportPanel>(ui(), sceneTarget, scene, selected);
|
||||||
|
|
||||||
panels().setDefaultLayout(
|
panels().setDefaultLayout(
|
||||||
{.left = {"Scene Hierarchy"},
|
{.left = {"Scene Hierarchy"},
|
||||||
@@ -31,6 +39,48 @@ class Editor : public sophia::Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onUpdate(const sophia::FrameInfo& frameInfo) override {}
|
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)
|
SOPHIA_MAIN(Editor)
|
||||||
|
|||||||
@@ -3,30 +3,135 @@
|
|||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <sophia/components.hpp>
|
||||||
|
|
||||||
void InspectorPanel::onUpdate(const sophia::FrameInfo&) {
|
void InspectorPanel::onUpdate(const sophia::FrameInfo&) {
|
||||||
char nameBuf[128];
|
if (!selected) {
|
||||||
std::snprintf(nameBuf, sizeof(nameBuf), "%s", name.c_str());
|
ImGui::TextDisabled("No entity selected");
|
||||||
if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) {
|
return;
|
||||||
name = nameBuf;
|
|
||||||
}
|
}
|
||||||
ImGui::Text("Tag: %s", tag.c_str());
|
|
||||||
|
drawTag();
|
||||||
|
ImGui::Separator();
|
||||||
|
drawTransform();
|
||||||
|
drawCamera();
|
||||||
|
drawSprite();
|
||||||
|
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
drawAddComponent();
|
||||||
|
}
|
||||||
|
|
||||||
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) {
|
void InspectorPanel::drawTag() {
|
||||||
ImGui::DragFloat3("Position", &position.x, 0.1f);
|
auto& tag = selected.getComponent<sophia::TagComponent>();
|
||||||
ImGui::DragFloat3("Rotation", &rotation.x, 0.1f);
|
|
||||||
ImGui::DragFloat3("Scale", &scale.x, 0.1f);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui::CollapsingHeader("Mesh Renderer",
|
char nameBuf[128];
|
||||||
ImGuiTreeNodeFlags_DefaultOpen)) {
|
std::snprintf(nameBuf, sizeof(nameBuf), "%s", tag.tag.c_str());
|
||||||
ImGui::Text("Mesh: %s", meshName.c_str());
|
if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) {
|
||||||
ImGui::Text("Material: %s", materialName.c_str());
|
tag.tag = nameBuf;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (ImGui::CollapsingHeader("Script", ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
||||||
ImGui::Text("%s", scriptName.c_str());
|
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", °rees, 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,31 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
#include <sophia/entity.hpp>
|
||||||
#include <sophia/frame_info.hpp>
|
#include <sophia/frame_info.hpp>
|
||||||
|
#include <sophia/handles.hpp>
|
||||||
#include <sophia/ui/panel.hpp>
|
#include <sophia/ui/panel.hpp>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
class InspectorPanel : public sophia::Panel {
|
class InspectorPanel : public sophia::Panel {
|
||||||
public:
|
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;
|
void onUpdate(const sophia::FrameInfo& frameInfo) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string name = "Player";
|
void drawTag();
|
||||||
std::string tag = "Player";
|
void drawTransform();
|
||||||
|
void drawCamera();
|
||||||
|
void drawSprite();
|
||||||
|
void drawAddComponent();
|
||||||
|
|
||||||
glm::vec3 position{0.0f, 1.0f, 0.0f};
|
sophia::Entity& selected;
|
||||||
glm::vec3 rotation{0.0f, 0.0f, 0.0f};
|
const TextureList& textures;
|
||||||
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";
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,31 +2,31 @@
|
|||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
||||||
void SceneHierarchyPanel::drawNode(const Node& node) {
|
#include <sophia/components.hpp>
|
||||||
if (node.children.empty()) {
|
|
||||||
|
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 flags = ImGuiTreeNodeFlags_Leaf |
|
||||||
ImGuiTreeNodeFlags_NoTreePushOnOpen |
|
ImGuiTreeNodeFlags_NoTreePushOnOpen |
|
||||||
ImGuiTreeNodeFlags_SpanAvailWidth;
|
ImGuiTreeNodeFlags_SpanAvailWidth;
|
||||||
if (node.name == selected) {
|
if (entity == selected) {
|
||||||
flags |= ImGuiTreeNodeFlags_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()) {
|
if (ImGui::IsItemClicked()) {
|
||||||
selected = node.name;
|
selected = entity;
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,20 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <sophia/entity.hpp>
|
||||||
#include <sophia/frame_info.hpp>
|
#include <sophia/frame_info.hpp>
|
||||||
|
#include <sophia/scene.hpp>
|
||||||
#include <sophia/ui/panel.hpp>
|
#include <sophia/ui/panel.hpp>
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class SceneHierarchyPanel : public sophia::Panel {
|
class SceneHierarchyPanel : public sophia::Panel {
|
||||||
public:
|
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;
|
void onUpdate(const sophia::FrameInfo& frameInfo) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Node {
|
sophia::Scene& scene;
|
||||||
std::string name;
|
sophia::Entity& selected;
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,86 @@
|
|||||||
|
|
||||||
#include <imgui.h>
|
#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&) {
|
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 struct {
|
||||||
const char* label;
|
const char* label;
|
||||||
GizmoMode mode;
|
GizmoMode mode;
|
||||||
@@ -17,20 +96,14 @@ void ViewportPanel::onUpdate(const sophia::FrameInfo&) {
|
|||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
}
|
}
|
||||||
ImGui::Checkbox("Grid", &showGrid);
|
ImGui::Checkbox("Grid", &showGrid);
|
||||||
|
}
|
||||||
|
|
||||||
ImGui::Separator();
|
void ViewportPanel::drawGrid(ImVec2 origin, ImVec2 size) {
|
||||||
|
|
||||||
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) {
|
|
||||||
constexpr float GRID_STEP = 40.0f;
|
constexpr float GRID_STEP = 40.0f;
|
||||||
const ImU32 gridColor = IM_COL32(255, 255, 255, 18);
|
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) {
|
for (float x = GRID_STEP; x < size.x; x += GRID_STEP) {
|
||||||
drawList->AddLine(ImVec2(origin.x + x, origin.y),
|
drawList->AddLine(ImVec2(origin.x + x, origin.y),
|
||||||
ImVec2(origin.x + x, end.y), gridColor);
|
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),
|
drawList->AddLine(ImVec2(origin.x, origin.y + y),
|
||||||
ImVec2(end.x, origin.y + y), gridColor);
|
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));
|
if (!ImGui::IsItemActive()) {
|
||||||
ImGui::TextDisabled("%.0f x %.0f", size.x, size.y);
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,64 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <sophia/entity.hpp>
|
||||||
#include <sophia/frame_info.hpp>
|
#include <sophia/frame_info.hpp>
|
||||||
#include <sophia/handles.hpp>
|
#include <sophia/handles.hpp>
|
||||||
|
#include <sophia/math.hpp>
|
||||||
|
#include <sophia/scene.hpp>
|
||||||
#include <sophia/ui/panel.hpp>
|
#include <sophia/ui/panel.hpp>
|
||||||
#include <sophia/ui/ui.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 {
|
class ViewportPanel : public sophia::Panel {
|
||||||
public:
|
public:
|
||||||
ViewportPanel(sophia::UI& ui, sophia::RenderTargetHandle target)
|
ViewportPanel(sophia::UI& ui, sophia::RenderTargetHandle target,
|
||||||
|
sophia::Scene& scene, sophia::Entity& selected)
|
||||||
: Panel{{.name = "Viewport",
|
: Panel{{.name = "Viewport",
|
||||||
.windowFlags = ImGuiWindowFlags_NoScrollbar |
|
.windowFlags = ImGuiWindowFlags_NoScrollbar |
|
||||||
ImGuiWindowFlags_NoScrollWithMouse}},
|
ImGuiWindowFlags_NoScrollWithMouse}},
|
||||||
ui{ui},
|
ui{ui},
|
||||||
target{target} {}
|
target{target},
|
||||||
|
scene{scene},
|
||||||
|
selected{selected} {}
|
||||||
|
|
||||||
void onUpdate(const sophia::FrameInfo& frameInfo) override;
|
void onUpdate(const sophia::FrameInfo& frameInfo) override;
|
||||||
|
|
||||||
private:
|
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::UI& ui;
|
||||||
sophia::RenderTargetHandle target;
|
sophia::RenderTargetHandle target;
|
||||||
|
sophia::Scene& scene;
|
||||||
|
sophia::Entity& selected;
|
||||||
|
|
||||||
enum class GizmoMode { Move, Rotate, Scale };
|
enum class GizmoMode { Move, Rotate, Scale };
|
||||||
|
|
||||||
GizmoMode gizmoMode = GizmoMode::Move;
|
GizmoMode gizmoMode = GizmoMode::Move;
|
||||||
bool showGrid = true;
|
bool showGrid = true;
|
||||||
|
// Set while a drag that started on the selected entity is moving it.
|
||||||
|
bool dragging = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,9 +24,15 @@ target_compile_definitions(${ENGINE_NAME} PUBLIC
|
|||||||
|
|
||||||
target_link_libraries(${ENGINE_NAME} PUBLIC
|
target_link_libraries(${ENGINE_NAME} PUBLIC
|
||||||
vulkan
|
vulkan
|
||||||
|
vma
|
||||||
shaderc
|
shaderc
|
||||||
glfw
|
glfw
|
||||||
glm
|
glm
|
||||||
imgui
|
imgui
|
||||||
entt
|
entt
|
||||||
|
Tracy::TracyClient
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(${ENGINE_NAME} PRIVATE
|
||||||
|
stb_image
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class Application {
|
|||||||
ResourceRegistryRef resources();
|
ResourceRegistryRef resources();
|
||||||
PanelRegistry& panels() { return this->panelRegistry; }
|
PanelRegistry& panels() { return this->panelRegistry; }
|
||||||
SceneRegistry& scenes() { return this->sceneRegistry; }
|
SceneRegistry& scenes() { return this->sceneRegistry; }
|
||||||
SceneViewRegistry& views() { return this->sceneViewRegistry; }
|
SceneViewRegistry& sceneViews() { return this->sceneViewRegistry; }
|
||||||
UI& ui() { return this->_ui; }
|
UI& ui() { return this->_ui; }
|
||||||
|
|
||||||
DebugUI& debugUI() { return this->_debugUI; }
|
DebugUI& debugUI() { return this->_debugUI; }
|
||||||
|
|||||||
@@ -47,6 +47,35 @@ struct MeshComponent {
|
|||||||
MeshHandle handle;
|
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 CameraComponent {
|
||||||
struct Orthographic {
|
struct Orthographic {
|
||||||
float height = 10.f;
|
float height = 10.f;
|
||||||
@@ -77,6 +106,15 @@ struct CameraComponent {
|
|||||||
return glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight,
|
return glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight,
|
||||||
ortho.nearPlane, ortho.farPlane);
|
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
|
} // namespace sophia
|
||||||
|
|||||||
@@ -59,4 +59,11 @@ class Entity {
|
|||||||
Scene* scene = nullptr;
|
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
|
} // namespace sophia
|
||||||
|
|||||||
@@ -38,11 +38,13 @@ class Panel;
|
|||||||
class RenderTarget;
|
class RenderTarget;
|
||||||
class Scene;
|
class Scene;
|
||||||
struct SceneView;
|
struct SceneView;
|
||||||
|
class Texture;
|
||||||
|
|
||||||
using MeshHandle = Handle<Mesh>;
|
using MeshHandle = Handle<Mesh>;
|
||||||
using PanelHandle = Handle<Panel>;
|
using PanelHandle = Handle<Panel>;
|
||||||
using RenderTargetHandle = Handle<RenderTarget>;
|
using RenderTargetHandle = Handle<RenderTarget>;
|
||||||
using SceneHandle = Handle<Scene>;
|
using SceneHandle = Handle<Scene>;
|
||||||
using SceneViewHandle = Handle<SceneView>;
|
using SceneViewHandle = Handle<SceneView>;
|
||||||
|
using TextureHandle = Handle<Texture>;
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ class ResourceRegistryRef {
|
|||||||
void requestResize(RenderTargetHandle handle, u32 width, u32 height);
|
void requestResize(RenderTargetHandle handle, u32 width, u32 height);
|
||||||
|
|
||||||
glm::vec2 getExtent(RenderTargetHandle handle) const;
|
glm::vec2 getExtent(RenderTargetHandle handle) const;
|
||||||
|
glm::vec2 getExtent(TextureHandle handle) const;
|
||||||
|
|
||||||
bool isAlive(RenderTargetHandle handle) const;
|
bool isAlive(RenderTargetHandle handle) const;
|
||||||
|
bool isAlive(TextureHandle handle) const;
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
void release(Handle<T> handle);
|
void release(Handle<T> handle);
|
||||||
@@ -45,16 +47,25 @@ class ResourceRegistryRef {
|
|||||||
RenderTargetHandle createResource(std::type_identity<RenderTarget>,
|
RenderTargetHandle createResource(std::type_identity<RenderTarget>,
|
||||||
u32 width = 1, u32 height = 1);
|
u32 width = 1, u32 height = 1);
|
||||||
|
|
||||||
|
TextureHandle createResource(std::type_identity<Texture>, u32 width,
|
||||||
|
u32 height, std::vector<u8> pixels);
|
||||||
|
|
||||||
ResourceRegistry* registry;
|
ResourceRegistry* registry;
|
||||||
};
|
};
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
MeshHandle ResourceRegistryRef::load<Mesh>(const std::string& path);
|
MeshHandle ResourceRegistryRef::load<Mesh>(const std::string& path);
|
||||||
|
|
||||||
|
template <>
|
||||||
|
TextureHandle ResourceRegistryRef::load<Texture>(const std::string& path);
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
void ResourceRegistryRef::release<Mesh>(MeshHandle handle);
|
void ResourceRegistryRef::release<Mesh>(MeshHandle handle);
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
void ResourceRegistryRef::release<RenderTarget>(RenderTargetHandle handle);
|
void ResourceRegistryRef::release<RenderTarget>(RenderTargetHandle handle);
|
||||||
|
|
||||||
|
template <>
|
||||||
|
void ResourceRegistryRef::release<Texture>(TextureHandle handle);
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
|
|||||||
@@ -13,13 +13,17 @@ class Scene {
|
|||||||
Scene();
|
Scene();
|
||||||
~Scene();
|
~Scene();
|
||||||
|
|
||||||
void createEntity(const std::string& name = {});
|
Entity createEntity(const std::string& name = {});
|
||||||
void destroyEntity(Entity entity);
|
void destroyEntity(Entity entity);
|
||||||
|
|
||||||
// The camera a render request uses when it doesn't name one. Null until
|
Entity createCamera(const std::string& name = "Camera");
|
||||||
// set.
|
|
||||||
entt::entity getActiveCamera() const { return activeCamera; }
|
/** Calls `fn(Entity)` for every live entity. Defined in entity.hpp. */
|
||||||
void setActiveCamera(entt::entity camera) { activeCamera = camera; }
|
template <typename Fn>
|
||||||
|
void forEachEntity(Fn&& fn);
|
||||||
|
|
||||||
|
Entity getActiveCamera();
|
||||||
|
void setActiveCamera(Entity camera);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class Entity;
|
friend class Entity;
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ class SceneRegistry {
|
|||||||
const Scene& get(SceneHandle handle) const { return pool.get(handle); }
|
const Scene& get(SceneHandle handle) const { return pool.get(handle); }
|
||||||
Scene& get(SceneHandle handle) { 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); }
|
void release(SceneHandle handle) { pool.release(handle); }
|
||||||
bool isAlive(SceneHandle handle) const { return pool.isAlive(handle); }
|
bool isAlive(SceneHandle handle) const { return pool.isAlive(handle); }
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ enum class ClearFlags : u8 {
|
|||||||
|
|
||||||
struct SceneView {
|
struct SceneView {
|
||||||
SceneHandle scene = INVALID_HANDLE;
|
SceneHandle scene = INVALID_HANDLE;
|
||||||
RenderTargetHandle target = INVALID_HANDLE;
|
RenderTargetHandle target =
|
||||||
Entity camera;
|
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};
|
vec4 viewport{0.0f, 0.0f, 1.0f, 1.0f};
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ class SceneViewRegistry {
|
|||||||
return pool.get(handle);
|
return pool.get(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SceneView& peek(SceneViewHandle handle) const {
|
||||||
|
return pool.get(handle);
|
||||||
|
}
|
||||||
|
|
||||||
void release(SceneViewHandle handle) {
|
void release(SceneViewHandle handle) {
|
||||||
isRenderQueueDirty = true;
|
isRenderQueueDirty = true;
|
||||||
pool.release(handle);
|
pool.release(handle);
|
||||||
|
|||||||
+40
-14
@@ -6,18 +6,20 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <set>
|
||||||
#include <sophia/application.hpp>
|
#include <sophia/application.hpp>
|
||||||
#include <sophia/util/logger.hpp>
|
#include <sophia/util/logger.hpp>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <tracy/Tracy.hpp>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
|
|
||||||
#include "rendering/imgui_renderer.hpp"
|
#include "renderer/imgui_renderer.hpp"
|
||||||
#include "rendering/model_render_system.hpp"
|
#include "renderer/model_render_system.hpp"
|
||||||
#include "rendering/render_context.hpp"
|
#include "renderer/render_context.hpp"
|
||||||
#include "rendering/render_graph.hpp"
|
#include "renderer/render_graph.hpp"
|
||||||
#include "rendering/scene_renderer.hpp"
|
#include "renderer/scene_renderer.hpp"
|
||||||
#include "resources/mesh.hpp"
|
#include "resources/mesh.hpp"
|
||||||
#include "resources/render_target.hpp"
|
#include "resources/render_target.hpp"
|
||||||
#include "resources/resource_registry.hpp"
|
#include "resources/resource_registry.hpp"
|
||||||
@@ -65,7 +67,8 @@ class Application::Impl {
|
|||||||
void run() {
|
void run() {
|
||||||
ModelRenderSystem modelRenderSystem{device, renderContext};
|
ModelRenderSystem modelRenderSystem{device, renderContext};
|
||||||
SceneRenderer sceneRenderer{this->application.sceneRegistry,
|
SceneRenderer sceneRenderer{this->application.sceneRegistry,
|
||||||
this->resourceRegistry};
|
this->resourceRegistry, device,
|
||||||
|
renderContext.getImageFormat()};
|
||||||
ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(),
|
ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(),
|
||||||
renderContext.getImageCount(),
|
renderContext.getImageCount(),
|
||||||
this->resourceRegistry};
|
this->resourceRegistry};
|
||||||
@@ -86,11 +89,12 @@ class Application::Impl {
|
|||||||
PanelRegistry& panelRegistry = this->application.panelRegistry;
|
PanelRegistry& panelRegistry = this->application.panelRegistry;
|
||||||
SceneRegistry& sceneRegistry = this->application.sceneRegistry;
|
SceneRegistry& sceneRegistry = this->application.sceneRegistry;
|
||||||
SceneViewRegistry& sceneViewRegistry = this->application.sceneViewRegistry;
|
SceneViewRegistry& sceneViewRegistry = this->application.sceneViewRegistry;
|
||||||
const SceneViewRegistry& sceneViewRegistryConst =
|
|
||||||
this->application.sceneViewRegistry;
|
|
||||||
|
|
||||||
imguiRenderer.setDefaultLayout(panelRegistry.getDefaultLayout());
|
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 startTime = std::chrono::high_resolution_clock::now();
|
||||||
auto currentTime = startTime;
|
auto currentTime = startTime;
|
||||||
u32 frameIndex = 0;
|
u32 frameIndex = 0;
|
||||||
@@ -115,7 +119,10 @@ class Application::Impl {
|
|||||||
glm::vec2(static_cast<float>(extent.width),
|
glm::vec2(static_cast<float>(extent.width),
|
||||||
static_cast<float>(extent.height)),
|
static_cast<float>(extent.height)),
|
||||||
this->application.camera};
|
this->application.camera};
|
||||||
|
{
|
||||||
|
ZoneScopedN("Application::onUpdate");
|
||||||
this->application.onUpdate(frameInfo);
|
this->application.onUpdate(frameInfo);
|
||||||
|
}
|
||||||
this->application.camera.setProjection(
|
this->application.camera.setProjection(
|
||||||
-frameInfo.currentFramebufferExtent.x / 2.f,
|
-frameInfo.currentFramebufferExtent.x / 2.f,
|
||||||
frameInfo.currentFramebufferExtent.x / 2.f,
|
frameInfo.currentFramebufferExtent.x / 2.f,
|
||||||
@@ -128,9 +135,13 @@ class Application::Impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
imguiRenderer.beginFrame();
|
imguiRenderer.beginFrame();
|
||||||
|
sceneRenderer.beginFrame(renderContext.getFrameIndex());
|
||||||
|
|
||||||
panelRegistry.update(frameInfo);
|
panelRegistry.update(frameInfo);
|
||||||
|
{
|
||||||
|
ZoneScopedN("Application::onRender");
|
||||||
this->application.onRender();
|
this->application.onRender();
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<ModelRenderSystem::MeshInstance> instances;
|
std::vector<ModelRenderSystem::MeshInstance> instances;
|
||||||
instances.reserve(this->meshes.size() + this->drawCalls.size());
|
instances.reserve(this->meshes.size() + this->drawCalls.size());
|
||||||
@@ -146,8 +157,8 @@ class Application::Impl {
|
|||||||
graph.importImage(renderContext.getCurrentImage(), Access::Present);
|
graph.importImage(renderContext.getCurrentImage(), Access::Present);
|
||||||
|
|
||||||
std::map<RenderTargetHandle, ImageRef> viewTargets;
|
std::map<RenderTargetHandle, ImageRef> viewTargets;
|
||||||
for (SceneViewHandle handle : sceneViewRegistryConst.GetRenderQueue()) {
|
for (SceneViewHandle handle : sceneViewRegistry.GetRenderQueue()) {
|
||||||
const SceneView& view = sceneViewRegistryConst.get(handle);
|
const SceneView& view = sceneViewRegistry.peek(handle);
|
||||||
if (!sceneRegistry.isAlive(view.scene) ||
|
if (!sceneRegistry.isAlive(view.scene) ||
|
||||||
!renderTargetPool.isAlive(view.target)) {
|
!renderTargetPool.isAlive(view.target)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -170,11 +181,24 @@ class Application::Impl {
|
|||||||
: vk::AttachmentLoadOp::eLoad,
|
: vk::AttachmentLoadOp::eLoad,
|
||||||
{color.r, color.g, color.b, color.a});
|
{color.r, color.g, color.b, color.a});
|
||||||
},
|
},
|
||||||
[&sceneRenderer, &sceneViewRegistry, handle](vk::CommandBuffer) {
|
[&sceneRenderer, &sceneRegistry, &sceneViewRegistry,
|
||||||
const SceneView& view = sceneViewRegistry.get(handle);
|
&viewsWithoutCamera, handle](vk::CommandBuffer commandBuffer) {
|
||||||
if (view.camera) {
|
ZoneScopedN("scene view pass");
|
||||||
sceneRenderer.render(view);
|
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) {
|
[&](vk::CommandBuffer commandBuffer) {
|
||||||
|
ZoneScopedN("main pass");
|
||||||
modelRenderSystem.render(commandBuffer, instances, frameInfo);
|
modelRenderSystem.render(commandBuffer, instances, frameInfo);
|
||||||
|
|
||||||
imguiRenderer.endFrame(commandBuffer);
|
imguiRenderer.endFrame(commandBuffer);
|
||||||
@@ -196,6 +221,7 @@ class Application::Impl {
|
|||||||
graph.execute(commandBuffer);
|
graph.execute(commandBuffer);
|
||||||
|
|
||||||
renderContext.endFrame();
|
renderContext.endFrame();
|
||||||
|
FrameMark;
|
||||||
|
|
||||||
frameIndex++;
|
frameIndex++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
#include "rendering/imgui_renderer.hpp"
|
#include "renderer/imgui_renderer.hpp"
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <imgui_impl_glfw.h>
|
#include <imgui_impl_glfw.h>
|
||||||
#include <imgui_impl_vulkan.h>
|
#include <imgui_impl_vulkan.h>
|
||||||
#include <imgui_internal.h>
|
#include <imgui_internal.h>
|
||||||
|
|
||||||
|
#include <tracy/Tracy.hpp>
|
||||||
|
|
||||||
#include "resources/render_target.hpp"
|
#include "resources/render_target.hpp"
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
@@ -79,6 +81,7 @@ ImGuiRenderer::~ImGuiRenderer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ImGuiRenderer::beginFrame() {
|
void ImGuiRenderer::beginFrame() {
|
||||||
|
ZoneScoped;
|
||||||
releaseStaleTextures();
|
releaseStaleTextures();
|
||||||
|
|
||||||
ImGui_ImplVulkan_NewFrame();
|
ImGui_ImplVulkan_NewFrame();
|
||||||
@@ -91,12 +94,13 @@ void ImGuiRenderer::beginFrame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImTextureID ImGuiRenderer::getTextureId(RenderTargetHandle handle) {
|
ImTextureID ImGuiRenderer::getTextureId(RenderTargetHandle handle) {
|
||||||
vk::ImageView view =
|
const RenderTarget& target = resources.renderTargetPool.get(handle);
|
||||||
resources.renderTargetPool.get(handle).getImage().getView();
|
vk::ImageView view = target.getImage().getView();
|
||||||
|
|
||||||
auto it = textures.find(handle.index);
|
auto it = textures.find(handle.index);
|
||||||
if (it != textures.end()) {
|
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;
|
return (ImTextureID)it->second.descriptorSet;
|
||||||
}
|
}
|
||||||
ImGui_ImplVulkan_RemoveTexture(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.
|
// left in once the scene pass has finished.
|
||||||
VkDescriptorSet descriptorSet = ImGui_ImplVulkan_AddTexture(
|
VkDescriptorSet descriptorSet = ImGui_ImplVulkan_AddTexture(
|
||||||
static_cast<VkImageView>(view), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
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;
|
return (ImTextureID)descriptorSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +125,7 @@ void ImGuiRenderer::releaseStaleTextures() {
|
|||||||
|
|
||||||
bool stale =
|
bool stale =
|
||||||
!renderTargetPool.isAlive(handle) ||
|
!renderTargetPool.isAlive(handle) ||
|
||||||
renderTargetPool.get(handle).getImage().getView() != it->second.view;
|
renderTargetPool.get(handle).getVersion() != it->second.version;
|
||||||
if (stale) {
|
if (stale) {
|
||||||
ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet);
|
ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet);
|
||||||
it = textures.erase(it);
|
it = textures.erase(it);
|
||||||
@@ -181,6 +186,7 @@ void ImGuiRenderer::applyLayoutIfNeeded(ImGuiID dockspaceId,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ImGuiRenderer::endFrame(vk::CommandBuffer commandBuffer) {
|
void ImGuiRenderer::endFrame(vk::CommandBuffer commandBuffer) {
|
||||||
|
ZoneScoped;
|
||||||
ImGui::Render();
|
ImGui::Render();
|
||||||
ImDrawData* mainDrawData = ImGui::GetDrawData();
|
ImDrawData* mainDrawData = ImGui::GetDrawData();
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ class ImGuiRenderer {
|
|||||||
|
|
||||||
struct Texture {
|
struct Texture {
|
||||||
u32 generation;
|
u32 generation;
|
||||||
vk::ImageView view;
|
u32 version;
|
||||||
VkDescriptorSet descriptorSet;
|
VkDescriptorSet descriptorSet;
|
||||||
};
|
};
|
||||||
|
|
||||||
+3
-1
@@ -1,6 +1,7 @@
|
|||||||
#include "rendering/model_render_system.hpp"
|
#include "renderer/model_render_system.hpp"
|
||||||
|
|
||||||
#include <sophia/util/assert.hpp>
|
#include <sophia/util/assert.hpp>
|
||||||
|
#include <tracy/Tracy.hpp>
|
||||||
|
|
||||||
#include "shaders/model_render_system.frag.spv.h"
|
#include "shaders/model_render_system.frag.spv.h"
|
||||||
#include "shaders/model_render_system.vert.spv.h"
|
#include "shaders/model_render_system.vert.spv.h"
|
||||||
@@ -44,6 +45,7 @@ ModelRenderSystem::~ModelRenderSystem() {
|
|||||||
void ModelRenderSystem::render(vk::CommandBuffer commandBuffer,
|
void ModelRenderSystem::render(vk::CommandBuffer commandBuffer,
|
||||||
std::span<const MeshInstance> instances,
|
std::span<const MeshInstance> instances,
|
||||||
const FrameInfo& frameInfo) {
|
const FrameInfo& frameInfo) {
|
||||||
|
ZoneScoped;
|
||||||
glm::mat4 viewProjection = frameInfo.camera.getViewProjection();
|
glm::mat4 viewProjection = frameInfo.camera.getViewProjection();
|
||||||
|
|
||||||
this->graphicsPipeline->bind(commandBuffer);
|
this->graphicsPipeline->bind(commandBuffer);
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
#include <sophia/frame_info.hpp>
|
#include <sophia/frame_info.hpp>
|
||||||
#include <span>
|
#include <span>
|
||||||
|
|
||||||
#include "rendering/render_context.hpp"
|
#include "renderer/render_context.hpp"
|
||||||
#include "resources/mesh.hpp"
|
#include "resources/mesh.hpp"
|
||||||
#include "vulkan/device.hpp"
|
#include "vulkan/device.hpp"
|
||||||
#include "vulkan/pipelines/graphics_pipeline.hpp"
|
#include "vulkan/pipelines/graphics_pipeline.hpp"
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
#include "rendering/render_context.hpp"
|
#include "renderer/render_context.hpp"
|
||||||
|
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <sophia/util/assert.hpp>
|
#include <sophia/util/assert.hpp>
|
||||||
#include <sophia/util/logger.hpp>
|
#include <sophia/util/logger.hpp>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <tracy/Tracy.hpp>
|
||||||
|
|
||||||
#include "vulkan/access.hpp"
|
#include "vulkan/access.hpp"
|
||||||
|
|
||||||
@@ -18,14 +19,19 @@ RenderContext::RenderContext(Window& window, Device& device)
|
|||||||
RenderContext::~RenderContext() { this->device.waitIdle(); }
|
RenderContext::~RenderContext() { this->device.waitIdle(); }
|
||||||
|
|
||||||
vk::CommandBuffer RenderContext::beginFrame() {
|
vk::CommandBuffer RenderContext::beginFrame() {
|
||||||
|
ZoneScoped;
|
||||||
SOPHIA_ASSERT(!this->isFrameStarted,
|
SOPHIA_ASSERT(!this->isFrameStarted,
|
||||||
"Can't call beginFrame while already in progress");
|
"Can't call beginFrame while already in progress");
|
||||||
|
|
||||||
FrameData& frame = this->frames[this->currentFrameIndex];
|
FrameData& frame = this->frames[this->currentFrameIndex];
|
||||||
|
|
||||||
vk::Fence inFlightFence = frame.inFlight.get();
|
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());
|
inFlightFence, vk::True, std::numeric_limits<u64>::max());
|
||||||
|
}
|
||||||
|
|
||||||
if (fenceResult != vk::Result::eSuccess) {
|
if (fenceResult != vk::Result::eSuccess) {
|
||||||
log::error("failed to wait for fence: " + vk::to_string(fenceResult));
|
log::error("failed to wait for fence: " + vk::to_string(fenceResult));
|
||||||
@@ -73,28 +79,35 @@ vk::CommandBuffer RenderContext::beginFrame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RenderContext::endFrame() {
|
void RenderContext::endFrame() {
|
||||||
|
ZoneScoped;
|
||||||
SOPHIA_ASSERT(this->isFrameStarted,
|
SOPHIA_ASSERT(this->isFrameStarted,
|
||||||
"Can't call endFrame while no frame is in progress");
|
"Can't call endFrame while no frame is in progress");
|
||||||
|
|
||||||
FrameData& frame = this->frames[this->currentFrameIndex];
|
FrameData& frame = this->frames[this->currentFrameIndex];
|
||||||
frame.commandBuffer.end();
|
frame.commandBuffer.end();
|
||||||
|
|
||||||
vk::Semaphore waitSemaphore = frame.imageAvailable.get();
|
|
||||||
vk::Semaphore signalSemaphore =
|
vk::Semaphore signalSemaphore =
|
||||||
this->renderFinishedSemaphores.at(this->currentImageIndex).get();
|
this->renderFinishedSemaphores.at(this->currentImageIndex).get();
|
||||||
vk::PipelineStageFlags waitStage = SWAPCHAIN_ACQUIRE_STAGE;
|
|
||||||
vk::CommandBuffer commandBuffer = frame.commandBuffer;
|
|
||||||
|
|
||||||
vk::SubmitInfo submitInfo{};
|
vk::SemaphoreSubmitInfo waitInfo{};
|
||||||
submitInfo.waitSemaphoreCount = 1;
|
waitInfo.semaphore = frame.imageAvailable.get();
|
||||||
submitInfo.pWaitSemaphores = &waitSemaphore;
|
waitInfo.stageMask = SWAPCHAIN_ACQUIRE_STAGE;
|
||||||
submitInfo.pWaitDstStageMask = &waitStage;
|
|
||||||
submitInfo.commandBufferCount = 1;
|
|
||||||
submitInfo.pCommandBuffers = &commandBuffer;
|
|
||||||
submitInfo.signalSemaphoreCount = 1;
|
|
||||||
submitInfo.pSignalSemaphores = &signalSemaphore;
|
|
||||||
|
|
||||||
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(
|
vk::Result result = this->swapchain->present(
|
||||||
this->device.getPresentQueue(), this->currentImageIndex, signalSemaphore);
|
this->device.getPresentQueue(), this->currentImageIndex, signalSemaphore);
|
||||||
@@ -39,6 +39,7 @@ class RenderContext {
|
|||||||
vk::Format getImageFormat() const {
|
vk::Format getImageFormat() const {
|
||||||
return this->swapchain->getImageFormat();
|
return this->swapchain->getImageFormat();
|
||||||
}
|
}
|
||||||
|
u32 getFrameIndex() const { return this->currentFrameIndex; }
|
||||||
u32 getImageCount() const {
|
u32 getImageCount() const {
|
||||||
return static_cast<u32>(this->swapchain->imageCount());
|
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 <sophia/util/assert.hpp>
|
||||||
|
#include <tracy/Tracy.hpp>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "vulkan/debug_label.hpp"
|
#include "vulkan/debug_label.hpp"
|
||||||
@@ -24,6 +25,7 @@ void RenderGraph::addPass(std::string name, const SetupFn& setup,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RenderGraph::execute(vk::CommandBuffer commandBuffer) {
|
void RenderGraph::execute(vk::CommandBuffer commandBuffer) {
|
||||||
|
ZoneScoped;
|
||||||
for (Pass& pass : this->passes) {
|
for (Pass& pass : this->passes) {
|
||||||
this->recordPass(commandBuffer, pass);
|
this->recordPass(commandBuffer, pass);
|
||||||
}
|
}
|
||||||
@@ -53,8 +55,6 @@ void RenderGraph::transitionTo(ImageRef ref, const AccessState& next,
|
|||||||
pending.barriers.push_back(makeImageBarrier(
|
pending.barriers.push_back(makeImageBarrier(
|
||||||
imported.image->get(), imported.image->getSubresourceRange(),
|
imported.image->get(), imported.image->getSubresourceRange(),
|
||||||
imported.state, next));
|
imported.state, next));
|
||||||
pending.srcStages |= imported.state.stage;
|
|
||||||
pending.dstStages |= next.stage;
|
|
||||||
imported.state = next;
|
imported.state = next;
|
||||||
} else {
|
} else {
|
||||||
// Same-layout read after read. Nothing to record, but a later write has
|
// 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,
|
void RenderGraph::flush(vk::CommandBuffer commandBuffer,
|
||||||
PendingBarriers& pending) {
|
PendingBarriers& pending) {
|
||||||
if (pending.barriers.empty()) {
|
recordImageBarriers(commandBuffer, pending.barriers);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
commandBuffer.pipelineBarrier(pending.srcStages, pending.dstStages, {},
|
|
||||||
nullptr, nullptr, pending.barriers);
|
|
||||||
pending = PendingBarriers{};
|
pending = PendingBarriers{};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,9 +104,7 @@ class RenderGraph {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct PendingBarriers {
|
struct PendingBarriers {
|
||||||
std::vector<vk::ImageMemoryBarrier> barriers;
|
std::vector<vk::ImageMemoryBarrier2> barriers;
|
||||||
vk::PipelineStageFlags srcStages;
|
|
||||||
vk::PipelineStageFlags dstStages;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void transitionTo(ImageRef ref, const AccessState& next,
|
void transitionTo(ImageRef ref, const AccessState& next,
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
@@ -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 "resources/pools/render_target_pool.hpp"
|
||||||
|
|
||||||
|
#include <tracy/Tracy.hpp>
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
RenderTargetPool::RenderTargetPool(Device& device, vk::Format format)
|
RenderTargetPool::RenderTargetPool(Device& device, vk::Format format)
|
||||||
@@ -16,6 +18,7 @@ void RenderTargetPool::release(RenderTargetHandle handle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RenderTargetPool::applyPendingResizes() {
|
void RenderTargetPool::applyPendingResizes() {
|
||||||
|
ZoneScoped;
|
||||||
pool.forEach([](RenderTarget& target) { target.applyPendingResize(); });
|
pool.forEach([](RenderTarget& target) { target.applyPendingResize(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ void RenderTarget::applyPendingResize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RenderTarget::create(u32 width, u32 height) {
|
void RenderTarget::create(u32 width, u32 height) {
|
||||||
|
this->version++;
|
||||||
this->image = Image::Builder(this->device)
|
this->image = Image::Builder(this->device)
|
||||||
.setExtent(width, height)
|
.setExtent(width, height)
|
||||||
.setFormat(this->format)
|
.setFormat(this->format)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class RenderTarget {
|
|||||||
Image& getImage() const { return *this->image; }
|
Image& getImage() const { return *this->image; }
|
||||||
|
|
||||||
vk::Extent2D getExtent() const;
|
vk::Extent2D getExtent() const;
|
||||||
|
u32 getVersion() const { return this->version; }
|
||||||
|
|
||||||
void requestResize(u32 width, u32 height);
|
void requestResize(u32 width, u32 height);
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ class RenderTarget {
|
|||||||
std::unique_ptr<Image> image;
|
std::unique_ptr<Image> image;
|
||||||
|
|
||||||
std::optional<vk::Extent2D> pendingExtent;
|
std::optional<vk::Extent2D> pendingExtent;
|
||||||
|
u32 version = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <sophia/handles.hpp>
|
#include <sophia/handles.hpp>
|
||||||
#include <sophia/util/pool.hpp>
|
#include <sophia/util/pool.hpp>
|
||||||
|
|
||||||
#include "resources/mesh.hpp"
|
#include "resources/mesh.hpp"
|
||||||
#include "resources/pools/render_target_pool.hpp"
|
#include "resources/pools/render_target_pool.hpp"
|
||||||
|
#include "resources/texture.hpp"
|
||||||
#include "vulkan/device.hpp"
|
#include "vulkan/device.hpp"
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
@@ -15,13 +17,20 @@ class ResourceRegistry {
|
|||||||
ResourceRegistry& operator=(const ResourceRegistry&) = delete;
|
ResourceRegistry& operator=(const ResourceRegistry&) = delete;
|
||||||
|
|
||||||
ResourceRegistry(Device& device, vk::Format renderTargetFormat)
|
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;
|
Device& device;
|
||||||
Pool<Mesh> meshPool{"Mesh"};
|
Pool<Mesh> meshPool{"Mesh"};
|
||||||
RenderTargetPool renderTargetPool;
|
RenderTargetPool renderTargetPool;
|
||||||
// TexturePool textures;
|
Pool<Texture> texturePool{"Texture"};
|
||||||
// MaterialPool materials;
|
// 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
|
} // namespace sophia
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
#include <stb_image.h>
|
||||||
|
|
||||||
#include <sophia/resource_registry_ref.hpp>
|
#include <sophia/resource_registry_ref.hpp>
|
||||||
#include <sophia/util/assert.hpp>
|
#include <sophia/util/assert.hpp>
|
||||||
|
#include <sophia/util/logger.hpp>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
#include "resources/resource_registry.hpp"
|
#include "resources/resource_registry.hpp"
|
||||||
|
|
||||||
@@ -14,6 +18,26 @@ MeshHandle ResourceRegistryRef::load<Mesh>(const std::string& path) {
|
|||||||
return MeshHandle{};
|
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 <>
|
template <>
|
||||||
void ResourceRegistryRef::release<Mesh>(MeshHandle handle) {
|
void ResourceRegistryRef::release<Mesh>(MeshHandle handle) {
|
||||||
registry->meshPool.release(handle);
|
registry->meshPool.release(handle);
|
||||||
@@ -24,6 +48,15 @@ void ResourceRegistryRef::release<RenderTarget>(RenderTargetHandle handle) {
|
|||||||
registry->renderTargetPool.release(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(
|
MeshHandle ResourceRegistryRef::createResource(
|
||||||
std::type_identity<Mesh>, std::vector<CanvasVertex> vertices,
|
std::type_identity<Mesh>, std::vector<CanvasVertex> vertices,
|
||||||
std::vector<u32> indices) {
|
std::vector<u32> indices) {
|
||||||
@@ -42,6 +75,13 @@ RenderTargetHandle ResourceRegistryRef::createResource(
|
|||||||
return registry->renderTargetPool.create(width, height);
|
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,
|
void ResourceRegistryRef::requestResize(RenderTargetHandle handle, u32 width,
|
||||||
u32 height) {
|
u32 height) {
|
||||||
registry->renderTargetPool.get(handle).requestResize(width, height);
|
registry->renderTargetPool.get(handle).requestResize(width, height);
|
||||||
@@ -56,4 +96,14 @@ bool ResourceRegistryRef::isAlive(RenderTargetHandle handle) const {
|
|||||||
return registry->renderTargetPool.isAlive(handle);
|
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
|
} // namespace sophia
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -1,16 +1,44 @@
|
|||||||
#include <sophia/components.hpp>
|
#include <sophia/components.hpp>
|
||||||
|
#include <sophia/entity.hpp>
|
||||||
#include <sophia/scene.hpp>
|
#include <sophia/scene.hpp>
|
||||||
#include <sophia/util/logger.hpp>
|
#include <sophia/util/logger.hpp>
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
Scene::Scene() {
|
Scene::Scene() {}
|
||||||
entt::entity entity = this->registry.create();
|
|
||||||
|
|
||||||
auto& a = this->registry.emplace<TransformComponent>(entity);
|
|
||||||
auto& b = this->registry.get<TransformComponent>(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
Scene::~Scene() { log::trace("destroying sophia::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
|
} // namespace sophia
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include <sophia/ui/panel_registry.hpp>
|
#include <sophia/ui/panel_registry.hpp>
|
||||||
|
#include <tracy/Tracy.hpp>
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ u32 PanelRegistry::visibleCount() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void PanelRegistry::update(const FrameInfo& frameInfo) {
|
void PanelRegistry::update(const FrameInfo& frameInfo) {
|
||||||
|
ZoneScoped;
|
||||||
pool.forEach([&frameInfo](Panel& panel) {
|
pool.forEach([&frameInfo](Panel& panel) {
|
||||||
if (panel.isVisible()) {
|
if (panel.isVisible()) {
|
||||||
panel.draw(frameInfo);
|
panel.draw(frameInfo);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#include <sophia/ui/ui.hpp>
|
#include <sophia/ui/ui.hpp>
|
||||||
#include <sophia/util/assert.hpp>
|
#include <sophia/util/assert.hpp>
|
||||||
|
|
||||||
#include "rendering/imgui_renderer.hpp"
|
#include "renderer/imgui_renderer.hpp"
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ constexpr std::array<Access, 7> ALL_ACCESSES = {
|
|||||||
Access::Present,
|
Access::Present,
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr vk::AccessFlags WRITE_ACCESSES =
|
constexpr vk::AccessFlags2 WRITE_ACCESSES =
|
||||||
vk::AccessFlagBits::eColorAttachmentWrite |
|
vk::AccessFlagBits2::eColorAttachmentWrite |
|
||||||
vk::AccessFlagBits::eDepthStencilAttachmentWrite |
|
vk::AccessFlagBits2::eDepthStencilAttachmentWrite |
|
||||||
vk::AccessFlagBits::eShaderWrite | vk::AccessFlagBits::eTransferWrite;
|
vk::AccessFlagBits2::eShaderStorageWrite |
|
||||||
|
vk::AccessFlagBits2::eTransferWrite;
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -25,34 +26,37 @@ AccessState getAccessState(Access access) {
|
|||||||
switch (access) {
|
switch (access) {
|
||||||
case Access::ColorWrite:
|
case Access::ColorWrite:
|
||||||
return {vk::ImageLayout::eColorAttachmentOptimal,
|
return {vk::ImageLayout::eColorAttachmentOptimal,
|
||||||
vk::PipelineStageFlagBits::eColorAttachmentOutput,
|
vk::PipelineStageFlagBits2::eColorAttachmentOutput,
|
||||||
vk::AccessFlagBits::eColorAttachmentRead |
|
vk::AccessFlagBits2::eColorAttachmentRead |
|
||||||
vk::AccessFlagBits::eColorAttachmentWrite};
|
vk::AccessFlagBits2::eColorAttachmentWrite};
|
||||||
case Access::DepthWrite:
|
case Access::DepthWrite:
|
||||||
return {vk::ImageLayout::eDepthStencilAttachmentOptimal,
|
return {vk::ImageLayout::eDepthStencilAttachmentOptimal,
|
||||||
vk::PipelineStageFlagBits::eEarlyFragmentTests |
|
vk::PipelineStageFlagBits2::eEarlyFragmentTests |
|
||||||
vk::PipelineStageFlagBits::eLateFragmentTests,
|
vk::PipelineStageFlagBits2::eLateFragmentTests,
|
||||||
vk::AccessFlagBits::eDepthStencilAttachmentRead |
|
vk::AccessFlagBits2::eDepthStencilAttachmentRead |
|
||||||
vk::AccessFlagBits::eDepthStencilAttachmentWrite};
|
vk::AccessFlagBits2::eDepthStencilAttachmentWrite};
|
||||||
case Access::SampledRead:
|
case Access::SampledRead:
|
||||||
return {vk::ImageLayout::eShaderReadOnlyOptimal,
|
return {vk::ImageLayout::eShaderReadOnlyOptimal,
|
||||||
vk::PipelineStageFlagBits::eFragmentShader,
|
vk::PipelineStageFlagBits2::eFragmentShader,
|
||||||
vk::AccessFlagBits::eShaderRead};
|
vk::AccessFlagBits2::eShaderSampledRead};
|
||||||
case Access::StorageReadWrite:
|
case Access::StorageReadWrite:
|
||||||
return {
|
return {vk::ImageLayout::eGeneral,
|
||||||
vk::ImageLayout::eGeneral, vk::PipelineStageFlagBits::eComputeShader,
|
vk::PipelineStageFlagBits2::eComputeShader,
|
||||||
vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite};
|
vk::AccessFlagBits2::eShaderStorageRead |
|
||||||
|
vk::AccessFlagBits2::eShaderStorageWrite};
|
||||||
case Access::TransferSrc:
|
case Access::TransferSrc:
|
||||||
return {vk::ImageLayout::eTransferSrcOptimal,
|
return {vk::ImageLayout::eTransferSrcOptimal,
|
||||||
vk::PipelineStageFlagBits::eTransfer,
|
vk::PipelineStageFlagBits2::eTransfer,
|
||||||
vk::AccessFlagBits::eTransferRead};
|
vk::AccessFlagBits2::eTransferRead};
|
||||||
case Access::TransferDst:
|
case Access::TransferDst:
|
||||||
return {vk::ImageLayout::eTransferDstOptimal,
|
return {vk::ImageLayout::eTransferDstOptimal,
|
||||||
vk::PipelineStageFlagBits::eTransfer,
|
vk::PipelineStageFlagBits2::eTransfer,
|
||||||
vk::AccessFlagBits::eTransferWrite};
|
vk::AccessFlagBits2::eTransferWrite};
|
||||||
case Access::Present:
|
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,
|
return {vk::ImageLayout::ePresentSrcKHR,
|
||||||
vk::PipelineStageFlagBits::eBottomOfPipe, vk::AccessFlags{}};
|
vk::PipelineStageFlagBits2::eNone, vk::AccessFlags2{}};
|
||||||
}
|
}
|
||||||
|
|
||||||
log::fatal("Unknown Access value.");
|
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);
|
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 vk::ImageSubresourceRange& range,
|
||||||
const AccessState& from,
|
const AccessState& from,
|
||||||
const AccessState& to) {
|
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.oldLayout = from.layout;
|
||||||
barrier.newLayout = to.layout;
|
barrier.newLayout = to.layout;
|
||||||
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||||
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||||
barrier.image = image;
|
barrier.image = image;
|
||||||
barrier.subresourceRange = range;
|
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;
|
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
|
} // namespace sophia
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ enum class Access {
|
|||||||
*/
|
*/
|
||||||
struct AccessState {
|
struct AccessState {
|
||||||
vk::ImageLayout layout = vk::ImageLayout::eUndefined;
|
vk::ImageLayout layout = vk::ImageLayout::eUndefined;
|
||||||
vk::PipelineStageFlags stage = vk::PipelineStageFlagBits::eTopOfPipe;
|
vk::PipelineStageFlags2 stage = vk::PipelineStageFlagBits2::eNone;
|
||||||
vk::AccessFlags access;
|
vk::AccessFlags2 access;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,8 +35,8 @@ struct AccessState {
|
|||||||
* just-acquired state has to use this as its source stage, otherwise the
|
* just-acquired state has to use this as its source stage, otherwise the
|
||||||
* layout transition isn't ordered after the acquire.
|
* layout transition isn't ordered after the acquire.
|
||||||
*/
|
*/
|
||||||
inline constexpr vk::PipelineStageFlags SWAPCHAIN_ACQUIRE_STAGE =
|
inline constexpr vk::PipelineStageFlags2 SWAPCHAIN_ACQUIRE_STAGE =
|
||||||
vk::PipelineStageFlagBits::eColorAttachmentOutput;
|
vk::PipelineStageFlagBits2::eColorAttachmentOutput;
|
||||||
|
|
||||||
AccessState getAccessState(Access access);
|
AccessState getAccessState(Access access);
|
||||||
|
|
||||||
@@ -55,9 +55,17 @@ bool writesImage(const AccessState& state);
|
|||||||
*/
|
*/
|
||||||
bool needsBarrier(const AccessState& from, const AccessState& to);
|
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 vk::ImageSubresourceRange& range,
|
||||||
const AccessState& from,
|
const AccessState& from,
|
||||||
const AccessState& to);
|
const AccessState& to);
|
||||||
|
|
||||||
|
void recordImageBarriers(
|
||||||
|
vk::CommandBuffer commandBuffer,
|
||||||
|
vk::ArrayProxy<const vk::ImageMemoryBarrier2> barriers);
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
|
|||||||
@@ -28,26 +28,32 @@ Buffer::Buffer(Device& device, vk::DeviceSize instanceSize, u32 instanceCount,
|
|||||||
alignmentSize = getAlignment(instanceSize, minOffsetAlignment);
|
alignmentSize = getAlignment(instanceSize, minOffsetAlignment);
|
||||||
bufferSize = alignmentSize * instanceCount;
|
bufferSize = alignmentSize * instanceCount;
|
||||||
device.createBuffer(bufferSize, usageFlags, memoryPropertyFlags, buffer,
|
device.createBuffer(bufferSize, usageFlags, memoryPropertyFlags, buffer,
|
||||||
memory);
|
allocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
Buffer::~Buffer() {
|
Buffer::~Buffer() {
|
||||||
unmap();
|
unmap();
|
||||||
this->device.get()->destroyBuffer(this->buffer);
|
vmaDestroyBuffer(this->device.getAllocator(), this->buffer, this->allocation);
|
||||||
this->device.get()->freeMemory(this->memory);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
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{};
|
void* base = nullptr;
|
||||||
return this->device.get()->mapMemory(this->memory, offset, size, emptyFlag,
|
vk::Result result = static_cast<vk::Result>(
|
||||||
&this->mapped);
|
vmaMapMemory(this->device.getAllocator(), this->allocation, &base));
|
||||||
|
if (result == vk::Result::eSuccess) {
|
||||||
|
this->mapped = static_cast<char*>(base) + offset;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Buffer::unmap() {
|
void Buffer::unmap() {
|
||||||
if (mapped) {
|
if (mapped) {
|
||||||
this->device.get()->unmapMemory(this->memory);
|
vmaUnmapMemory(this->device.getAllocator(), this->allocation);
|
||||||
mapped = nullptr;
|
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::Result Buffer::flush(vk::DeviceSize size, vk::DeviceSize offset) {
|
||||||
vk::MappedMemoryRange mappedRange = {};
|
return static_cast<vk::Result>(vmaFlushAllocation(
|
||||||
mappedRange.memory = memory;
|
this->device.getAllocator(), this->allocation, offset, size));
|
||||||
mappedRange.offset = offset;
|
|
||||||
mappedRange.size = size;
|
|
||||||
return this->device.get()->flushMappedMemoryRanges(1, &mappedRange);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vk::Result Buffer::invalidate(vk::DeviceSize size, vk::DeviceSize offset) {
|
vk::Result Buffer::invalidate(vk::DeviceSize size, vk::DeviceSize offset) {
|
||||||
vk::MappedMemoryRange mappedRange = {};
|
return static_cast<vk::Result>(vmaInvalidateAllocation(
|
||||||
mappedRange.memory = memory;
|
this->device.getAllocator(), this->allocation, offset, size));
|
||||||
mappedRange.offset = offset;
|
|
||||||
mappedRange.size = size;
|
|
||||||
return this->device.get()->invalidateMappedMemoryRanges(1, &mappedRange);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vk::DescriptorBufferInfo Buffer::descriptorInfo(vk::DeviceSize size,
|
vk::DescriptorBufferInfo Buffer::descriptorInfo(vk::DeviceSize size,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class Buffer {
|
|||||||
Device& device;
|
Device& device;
|
||||||
void* mapped = nullptr;
|
void* mapped = nullptr;
|
||||||
vk::Buffer buffer = VK_NULL_HANDLE;
|
vk::Buffer buffer = VK_NULL_HANDLE;
|
||||||
vk::DeviceMemory memory = VK_NULL_HANDLE;
|
VmaAllocation allocation = VK_NULL_HANDLE;
|
||||||
|
|
||||||
vk::DeviceSize bufferSize;
|
vk::DeviceSize bufferSize;
|
||||||
uint32_t instanceCount;
|
uint32_t instanceCount;
|
||||||
|
|||||||
+125
-77
@@ -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)
|
Device::Device(Window* window)
|
||||||
: headless{window == nullptr},
|
: headless{window == nullptr},
|
||||||
enabledExtensions{headless ? std::vector<const char*>{}
|
enabledExtensions{headless ? std::vector<const char*>{}
|
||||||
@@ -69,10 +139,14 @@ Device::Device(Window* window)
|
|||||||
}
|
}
|
||||||
pickPhysicalDevice();
|
pickPhysicalDevice();
|
||||||
createLogicalDevice();
|
createLogicalDevice();
|
||||||
|
createAllocator();
|
||||||
createCommandPool();
|
createCommandPool();
|
||||||
}
|
}
|
||||||
|
|
||||||
Device::~Device() {
|
Device::~Device() {
|
||||||
|
vmaDestroyAllocator(this->allocator);
|
||||||
|
log::trace("destroyed VmaAllocator");
|
||||||
|
|
||||||
if (this->enableValidationLayers) {
|
if (this->enableValidationLayers) {
|
||||||
destroyDebugUtilsMessengerEXT(this->instance.get(), this->debugMessenger,
|
destroyDebugUtilsMessengerEXT(this->instance.get(), this->debugMessenger,
|
||||||
nullptr);
|
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(
|
vk::Format Device::findSupportedFormat(
|
||||||
const std::vector<vk::Format>& candidates, vk::ImageTiling tiling,
|
const std::vector<vk::Format>& candidates, vk::ImageTiling tiling,
|
||||||
vk::FormatFeatureFlags features) {
|
vk::FormatFeatureFlags features) {
|
||||||
@@ -125,35 +183,26 @@ vk::Format Device::findSupportedFormat(
|
|||||||
|
|
||||||
void Device::createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage,
|
void Device::createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage,
|
||||||
vk::MemoryPropertyFlags properties,
|
vk::MemoryPropertyFlags properties,
|
||||||
vk::Buffer& buffer, vk::DeviceMemory& bufferMemory) {
|
vk::Buffer& buffer, VmaAllocation& allocation) {
|
||||||
vk::BufferCreateInfo bufferInfo = {};
|
vk::BufferCreateInfo bufferInfo = {};
|
||||||
bufferInfo.size = size;
|
bufferInfo.size = size;
|
||||||
bufferInfo.usage = usage;
|
bufferInfo.usage = usage;
|
||||||
bufferInfo.sharingMode = vk::SharingMode::eExclusive;
|
bufferInfo.sharingMode = vk::SharingMode::eExclusive;
|
||||||
|
|
||||||
try {
|
VmaAllocationCreateInfo allocInfo = {};
|
||||||
buffer = this->device->createBuffer(bufferInfo);
|
allocInfo.requiredFlags = static_cast<VkMemoryPropertyFlags>(properties);
|
||||||
} catch (const vk::SystemError& error) {
|
|
||||||
log::fatal("failed to create vertex buffer");
|
VkBuffer rawBuffer = VK_NULL_HANDLE;
|
||||||
throw std::runtime_error("failed to create vertex buffer");
|
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 =
|
buffer = rawBuffer;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vk::CommandBuffer Device::beginSingleTimeCommands() {
|
vk::CommandBuffer Device::beginSingleTimeCommands() {
|
||||||
@@ -220,43 +269,21 @@ void Device::copyBuffer(vk::Buffer sourceBuffer, vk::Buffer destinationBuffer,
|
|||||||
|
|
||||||
void Device::createImageWithInfo(const vk::ImageCreateInfo& imageInfo,
|
void Device::createImageWithInfo(const vk::ImageCreateInfo& imageInfo,
|
||||||
vk::MemoryPropertyFlags properties,
|
vk::MemoryPropertyFlags properties,
|
||||||
vk::Image& image,
|
vk::Image& image, VmaAllocation& allocation) {
|
||||||
vk::DeviceMemory& imageMemory) {
|
VmaAllocationCreateInfo allocInfo = {};
|
||||||
try {
|
allocInfo.requiredFlags = static_cast<VkMemoryPropertyFlags>(properties);
|
||||||
vk::Result result = this->device->createImage(&imageInfo, nullptr, &image);
|
|
||||||
if (result != vk::Result::eSuccess) {
|
VkImage rawImage = VK_NULL_HANDLE;
|
||||||
throw vk::SystemError(result);
|
VkResult result = vmaCreateImage(
|
||||||
}
|
this->allocator, &static_cast<const VkImageCreateInfo&>(imageInfo),
|
||||||
} catch (const vk::SystemError& error) {
|
&allocInfo, &rawImage, &allocation, nullptr);
|
||||||
log::fatal("failed to create image. Error: ", error.what());
|
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");
|
throw std::runtime_error("failed to create image");
|
||||||
}
|
}
|
||||||
|
|
||||||
vk::MemoryRequirements memoryRequirements =
|
image = rawImage;
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Device::populateImGuiInitInfo(ImGui_ImplVulkan_InitInfo& initInfo) {
|
void Device::populateImGuiInitInfo(ImGui_ImplVulkan_InitInfo& initInfo) {
|
||||||
@@ -429,6 +456,10 @@ bool Device::isPhysicalDeviceSuitable(const vk::PhysicalDevice& device) {
|
|||||||
|
|
||||||
bool extensionsSupported = checkDeviceExtensionSupport(device);
|
bool extensionsSupported = checkDeviceExtensionSupport(device);
|
||||||
|
|
||||||
|
if (!checkDeviceFeatureSupport(device)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (this->headless) {
|
if (this->headless) {
|
||||||
return indices.graphicsFamily.has_value() && extensionsSupported;
|
return indices.graphicsFamily.has_value() && extensionsSupported;
|
||||||
}
|
}
|
||||||
@@ -486,15 +517,17 @@ void Device::createLogicalDevice() {
|
|||||||
{vk::DeviceQueueCreateFlags(), queueFamily, 1, &queuePriority});
|
{vk::DeviceQueueCreateFlags(), queueFamily, 1, &queuePriority});
|
||||||
}
|
}
|
||||||
|
|
||||||
vk::PhysicalDeviceVulkan13Features vulkan13Features{};
|
DeviceFeatureChain enabledFeatures;
|
||||||
vulkan13Features.dynamicRendering = vk::True;
|
forEachRequiredFeature(enabledFeatures, [](vk::Bool32& feature, const char*) {
|
||||||
|
feature = vk::True;
|
||||||
|
});
|
||||||
|
|
||||||
auto deviceFeatures = vk::PhysicalDeviceFeatures();
|
|
||||||
auto createInfo = vk::DeviceCreateInfo(
|
auto createInfo = vk::DeviceCreateInfo(
|
||||||
vk::DeviceCreateFlags(), static_cast<uint32_t>(queueCreateInfos.size()),
|
vk::DeviceCreateFlags(), static_cast<uint32_t>(queueCreateInfos.size()),
|
||||||
queueCreateInfos.data());
|
queueCreateInfos.data());
|
||||||
createInfo.pEnabledFeatures = &deviceFeatures;
|
// Core features are passed through PhysicalDeviceFeatures2 in the pNext
|
||||||
createInfo.pNext = &vulkan13Features;
|
// chain, so pEnabledFeatures must stay null.
|
||||||
|
createInfo.pNext = &enabledFeatures.get<vk::PhysicalDeviceFeatures2>();
|
||||||
|
|
||||||
createInfo.enabledExtensionCount =
|
createInfo.enabledExtensionCount =
|
||||||
static_cast<uint32_t>(this->enabledExtensions.size());
|
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(
|
SwapchainSupportDetails Device::querySwapchainSupport(
|
||||||
vk::PhysicalDevice device) {
|
vk::PhysicalDevice device) {
|
||||||
SwapchainSupportDetails details;
|
SwapchainSupportDetails details;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <imgui_impl_vulkan.h>
|
#include <imgui_impl_vulkan.h>
|
||||||
|
#include <vk_mem_alloc.h>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
@@ -47,6 +48,7 @@ class Device {
|
|||||||
|
|
||||||
void waitIdle() { this->device->waitIdle(); }
|
void waitIdle() { this->device->waitIdle(); }
|
||||||
|
|
||||||
|
VmaAllocator getAllocator() const { return this->allocator; }
|
||||||
vk::CommandPool getCommandPool() const { return this->commandPool; }
|
vk::CommandPool getCommandPool() const { return this->commandPool; }
|
||||||
vk::SurfaceKHR getSurface() const { return surface; }
|
vk::SurfaceKHR getSurface() const { return surface; }
|
||||||
vk::Queue getGraphicsQueue() const { return graphicsQueue; }
|
vk::Queue getGraphicsQueue() const { return graphicsQueue; }
|
||||||
@@ -60,15 +62,13 @@ class Device {
|
|||||||
return querySwapchainSupport(this->physicalDevice);
|
return querySwapchainSupport(this->physicalDevice);
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 findMemoryType(u32 typeFilter, vk::MemoryPropertyFlags properties);
|
|
||||||
|
|
||||||
vk::Format findSupportedFormat(const std::vector<vk::Format>& candidates,
|
vk::Format findSupportedFormat(const std::vector<vk::Format>& candidates,
|
||||||
vk::ImageTiling tiling,
|
vk::ImageTiling tiling,
|
||||||
vk::FormatFeatureFlags features);
|
vk::FormatFeatureFlags features);
|
||||||
|
|
||||||
void createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage,
|
void createBuffer(vk::DeviceSize size, vk::BufferUsageFlags usage,
|
||||||
vk::MemoryPropertyFlags properties, vk::Buffer& buffer,
|
vk::MemoryPropertyFlags properties, vk::Buffer& buffer,
|
||||||
vk::DeviceMemory& bufferMemory);
|
VmaAllocation& allocation);
|
||||||
|
|
||||||
vk::CommandBuffer beginSingleTimeCommands();
|
vk::CommandBuffer beginSingleTimeCommands();
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ class Device {
|
|||||||
|
|
||||||
void createImageWithInfo(const vk::ImageCreateInfo& imageInfo,
|
void createImageWithInfo(const vk::ImageCreateInfo& imageInfo,
|
||||||
vk::MemoryPropertyFlags properties, vk::Image& image,
|
vk::MemoryPropertyFlags properties, vk::Image& image,
|
||||||
vk::DeviceMemory& imageMemory);
|
VmaAllocation& allocation);
|
||||||
|
|
||||||
void populateImGuiInitInfo(ImGui_ImplVulkan_InitInfo& initInfo);
|
void populateImGuiInitInfo(ImGui_ImplVulkan_InitInfo& initInfo);
|
||||||
|
|
||||||
@@ -100,6 +100,7 @@ class Device {
|
|||||||
SwapchainSupportDetails querySwapchainSupport(vk::PhysicalDevice device);
|
SwapchainSupportDetails querySwapchainSupport(vk::PhysicalDevice device);
|
||||||
|
|
||||||
void createLogicalDevice();
|
void createLogicalDevice();
|
||||||
|
void createAllocator();
|
||||||
void createCommandPool();
|
void createCommandPool();
|
||||||
|
|
||||||
vk::UniqueInstance instance;
|
vk::UniqueInstance instance;
|
||||||
@@ -111,6 +112,8 @@ class Device {
|
|||||||
vk::PhysicalDevice physicalDevice = VK_NULL_HANDLE;
|
vk::PhysicalDevice physicalDevice = VK_NULL_HANDLE;
|
||||||
vk::UniqueDevice device;
|
vk::UniqueDevice device;
|
||||||
|
|
||||||
|
VmaAllocator allocator = VK_NULL_HANDLE;
|
||||||
|
|
||||||
vk::Queue graphicsQueue;
|
vk::Queue graphicsQueue;
|
||||||
vk::Queue presentQueue;
|
vk::Queue presentQueue;
|
||||||
|
|
||||||
|
|||||||
+36
-23
@@ -3,11 +3,35 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <sophia/util/logger.hpp>
|
#include <sophia/util/logger.hpp>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include "vulkan/device.hpp"
|
#include "vulkan/device.hpp"
|
||||||
|
|
||||||
namespace sophia {
|
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 {
|
vk::ImageSubresourceRange Image::getSubresourceRange() const {
|
||||||
return vk::ImageSubresourceRange{this->aspectMask, 0, this->mipLevels, 0,
|
return vk::ImageSubresourceRange{this->aspectMask, 0, this->mipLevels, 0,
|
||||||
this->arrayLayers};
|
this->arrayLayers};
|
||||||
@@ -32,10 +56,9 @@ void Image::transitionLayout(vk::CommandBuffer commandBuffer,
|
|||||||
AccessState to = getStateForLayout(newLayout);
|
AccessState to = getStateForLayout(newLayout);
|
||||||
|
|
||||||
if (needsBarrier(from, to)) {
|
if (needsBarrier(from, to)) {
|
||||||
vk::ImageMemoryBarrier barrier =
|
vk::ImageMemoryBarrier2 barrier =
|
||||||
makeImageBarrier(this->get(), this->getSubresourceRange(), from, to);
|
makeImageBarrier(this->get(), this->getSubresourceRange(), from, to);
|
||||||
commandBuffer.pipelineBarrier(from.stage, to.stage, {}, nullptr, nullptr,
|
recordImageBarriers(commandBuffer, barrier);
|
||||||
barrier);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this->layout = newLayout;
|
this->layout = newLayout;
|
||||||
@@ -194,35 +217,26 @@ std::unique_ptr<Image> Image::Builder::build() {
|
|||||||
imageInfo.sharingMode = vk::SharingMode::eExclusive;
|
imageInfo.sharingMode = vk::SharingMode::eExclusive;
|
||||||
imageInfo.samples = this->samples;
|
imageInfo.samples = this->samples;
|
||||||
|
|
||||||
result->image = this->device.get()->createImageUnique(imageInfo);
|
this->device.createImageWithInfo(imageInfo, this->memoryProperties,
|
||||||
|
result->image, result->allocation);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this->isExternalImage && this->initialData != nullptr) {
|
if (!this->isExternalImage && this->initialData != nullptr) {
|
||||||
|
VmaAllocator allocator = this->device.getAllocator();
|
||||||
|
|
||||||
vk::Buffer stagingBuffer;
|
vk::Buffer stagingBuffer;
|
||||||
vk::DeviceMemory stagingMemory;
|
VmaAllocation stagingAllocation;
|
||||||
this->device.createBuffer(this->initialDataSize,
|
this->device.createBuffer(this->initialDataSize,
|
||||||
vk::BufferUsageFlagBits::eTransferSrc,
|
vk::BufferUsageFlagBits::eTransferSrc,
|
||||||
vk::MemoryPropertyFlagBits::eHostVisible |
|
vk::MemoryPropertyFlagBits::eHostVisible |
|
||||||
vk::MemoryPropertyFlagBits::eHostCoherent,
|
vk::MemoryPropertyFlagBits::eHostCoherent,
|
||||||
stagingBuffer, stagingMemory);
|
stagingBuffer, stagingAllocation);
|
||||||
|
|
||||||
void* mapped =
|
void* mapped = nullptr;
|
||||||
this->device.get()->mapMemory(stagingMemory, 0, this->initialDataSize);
|
vmaMapMemory(allocator, stagingAllocation, &mapped);
|
||||||
memcpy(mapped, this->initialData,
|
memcpy(mapped, this->initialData,
|
||||||
static_cast<size_t>(this->initialDataSize));
|
static_cast<size_t>(this->initialDataSize));
|
||||||
this->device.get()->unmapMemory(stagingMemory);
|
vmaUnmapMemory(allocator, stagingAllocation);
|
||||||
|
|
||||||
vk::BufferImageCopy region{};
|
vk::BufferImageCopy region{};
|
||||||
region.bufferOffset = 0;
|
region.bufferOffset = 0;
|
||||||
@@ -247,8 +261,7 @@ std::unique_ptr<Image> Image::Builder::build() {
|
|||||||
}
|
}
|
||||||
this->device.endSingleTimeCommands(commandBuffer);
|
this->device.endSingleTimeCommands(commandBuffer);
|
||||||
|
|
||||||
this->device.get()->destroyBuffer(stagingBuffer);
|
vmaDestroyBuffer(allocator, stagingBuffer, stagingAllocation);
|
||||||
this->device.get()->freeMemory(stagingMemory);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->createView) {
|
if (this->createView) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <vk_mem_alloc.h>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <sophia/types.hpp>
|
#include <sophia/types.hpp>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
@@ -16,11 +18,12 @@ class Image {
|
|||||||
|
|
||||||
Image(const Image&) = delete;
|
Image(const Image&) = delete;
|
||||||
Image& operator=(const Image&) = delete;
|
Image& operator=(const Image&) = delete;
|
||||||
Image(Image&&) = default;
|
Image(Image&& other) noexcept;
|
||||||
Image& operator=(Image&&) = delete;
|
Image& operator=(Image&&) = delete;
|
||||||
|
~Image();
|
||||||
|
|
||||||
vk::Image get() const {
|
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::ImageView getView() const { return this->view.get(); }
|
||||||
vk::Sampler getSampler() const { return this->sampler.get(); }
|
vk::Sampler getSampler() const { return this->sampler.get(); }
|
||||||
@@ -56,8 +59,8 @@ class Image {
|
|||||||
Device& device;
|
Device& device;
|
||||||
|
|
||||||
vk::Image externalImage;
|
vk::Image externalImage;
|
||||||
vk::UniqueImage image;
|
vk::Image image;
|
||||||
vk::UniqueDeviceMemory memory;
|
VmaAllocation allocation = VK_NULL_HANDLE;
|
||||||
vk::UniqueImageView view;
|
vk::UniqueImageView view;
|
||||||
vk::UniqueSampler sampler;
|
vk::UniqueSampler sampler;
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,12 @@ GraphicsPipeline::Builder& GraphicsPipeline::Builder::setVertexInput(
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GraphicsPipeline::Builder& GraphicsPipeline::Builder::setAlphaBlending(
|
||||||
|
bool enabled) {
|
||||||
|
this->alphaBlending = enabled;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
std::unique_ptr<GraphicsPipeline> GraphicsPipeline::Builder::build() const {
|
std::unique_ptr<GraphicsPipeline> GraphicsPipeline::Builder::build() const {
|
||||||
if (this->vertexShader == nullptr || this->fragmentShader == nullptr) {
|
if (this->vertexShader == nullptr || this->fragmentShader == nullptr) {
|
||||||
log::fatal(
|
log::fatal(
|
||||||
@@ -128,7 +134,8 @@ std::unique_ptr<GraphicsPipeline> GraphicsPipeline::Builder::build() const {
|
|||||||
this->device, *this->vertexShader, *this->fragmentShader,
|
this->device, *this->vertexShader, *this->fragmentShader,
|
||||||
this->pipelineLayout, this->colorAttachmentFormats,
|
this->pipelineLayout, this->colorAttachmentFormats,
|
||||||
this->depthAttachmentFormat, this->stencilAttachmentFormat,
|
this->depthAttachmentFormat, this->stencilAttachmentFormat,
|
||||||
this->bindingDescriptions, this->attributeDescriptions));
|
this->bindingDescriptions, this->attributeDescriptions,
|
||||||
|
this->alphaBlending));
|
||||||
}
|
}
|
||||||
|
|
||||||
GraphicsPipeline::GraphicsPipeline(
|
GraphicsPipeline::GraphicsPipeline(
|
||||||
@@ -137,10 +144,21 @@ GraphicsPipeline::GraphicsPipeline(
|
|||||||
std::vector<vk::Format> colorAttachmentFormats,
|
std::vector<vk::Format> colorAttachmentFormats,
|
||||||
vk::Format depthAttachmentFormat, vk::Format stencilAttachmentFormat,
|
vk::Format depthAttachmentFormat, vk::Format stencilAttachmentFormat,
|
||||||
std::vector<vk::VertexInputBindingDescription> bindingDescriptions,
|
std::vector<vk::VertexInputBindingDescription> bindingDescriptions,
|
||||||
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions)
|
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions,
|
||||||
|
bool alphaBlending)
|
||||||
: Pipeline(device, vk::PipelineBindPoint::eGraphics) {
|
: Pipeline(device, vk::PipelineBindPoint::eGraphics) {
|
||||||
setDefaultGraphicsPipelineConfig();
|
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.bindingDescriptions = std::move(bindingDescriptions);
|
||||||
this->config.attributeDescriptions = std::move(attributeDescriptions);
|
this->config.attributeDescriptions = std::move(attributeDescriptions);
|
||||||
this->config.pipelineLayout = pipelineLayout;
|
this->config.pipelineLayout = pipelineLayout;
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ class GraphicsPipeline : public Pipeline {
|
|||||||
std::vector<vk::Format> colorAttachmentFormats,
|
std::vector<vk::Format> colorAttachmentFormats,
|
||||||
vk::Format depthAttachmentFormat, vk::Format stencilAttachmentFormat,
|
vk::Format depthAttachmentFormat, vk::Format stencilAttachmentFormat,
|
||||||
std::vector<vk::VertexInputBindingDescription> bindingDescriptions,
|
std::vector<vk::VertexInputBindingDescription> bindingDescriptions,
|
||||||
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions);
|
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions,
|
||||||
|
bool alphaBlending);
|
||||||
|
|
||||||
void setDefaultGraphicsPipelineConfig();
|
void setDefaultGraphicsPipelineConfig();
|
||||||
|
|
||||||
@@ -69,6 +70,8 @@ class GraphicsPipeline::Builder {
|
|||||||
Builder& setVertexInput(
|
Builder& setVertexInput(
|
||||||
std::vector<vk::VertexInputBindingDescription> bindingDescriptions,
|
std::vector<vk::VertexInputBindingDescription> bindingDescriptions,
|
||||||
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions);
|
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;
|
std::unique_ptr<GraphicsPipeline> build() const;
|
||||||
|
|
||||||
@@ -83,6 +86,7 @@ class GraphicsPipeline::Builder {
|
|||||||
vk::Format stencilAttachmentFormat = vk::Format::eUndefined;
|
vk::Format stencilAttachmentFormat = vk::Format::eUndefined;
|
||||||
std::vector<vk::VertexInputBindingDescription> bindingDescriptions{};
|
std::vector<vk::VertexInputBindingDescription> bindingDescriptions{};
|
||||||
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions{};
|
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions{};
|
||||||
|
bool alphaBlending = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#define VMA_IMPLEMENTATION
|
||||||
|
#include <vk_mem_alloc.h>
|
||||||
+95
-20
@@ -1,3 +1,5 @@
|
|||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -7,8 +9,12 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <sophia/application.hpp>
|
#include <sophia/application.hpp>
|
||||||
|
#include <sophia/components.hpp>
|
||||||
|
#include <sophia/entity.hpp>
|
||||||
#include <sophia/event.hpp>
|
#include <sophia/event.hpp>
|
||||||
#include <sophia/key_event.hpp>
|
#include <sophia/key_event.hpp>
|
||||||
|
#include <sophia/scene_view.hpp>
|
||||||
|
#include <sophia/ui/panel.hpp>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -16,6 +22,24 @@
|
|||||||
struct Boid {
|
struct Boid {
|
||||||
glm::vec2 position;
|
glm::vec2 position;
|
||||||
glm::vec2 velocity;
|
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;
|
constexpr uint32_t DEFAULT_NUMBER_OF_BOIDS = 25;
|
||||||
@@ -63,6 +87,8 @@ class Boids2D : public sophia::Application {
|
|||||||
[this](sophia::KeyReleasedEvent& e) {
|
[this](sophia::KeyReleasedEvent& e) {
|
||||||
setScrollKeyHeld(e.getKeyCode(), false);
|
setScrollKeyHeld(e.getKeyCode(), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
debugUI().show();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onCreate() override {
|
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.velocity = glm::vec2{std::cos(theta), std::sin(theta)} * MAX_SPEED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Boid shape in local space, nose pointing along +x; each boid is drawn
|
boidTexture = createBoidTexture();
|
||||||
// as an instance of this one mesh with a per-boid transform.
|
|
||||||
std::vector<sophia::CanvasVertex> vertices{
|
sceneHandle = scenes().create();
|
||||||
{{BOID_LENGTH * 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}},
|
sophia::Scene& scene = scenes().get(sceneHandle);
|
||||||
{{-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}},
|
cameraEntity = scene.createCamera();
|
||||||
};
|
|
||||||
boidMesh = resources().create<sophia::Mesh>(std::move(vertices));
|
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 {
|
void onUpdate(const sophia::FrameInfo& frameInfo) override {
|
||||||
float scrollDistance =
|
float scrollDistance =
|
||||||
SCROLL_SPEED * static_cast<float>(frameInfo.deltaTime);
|
SCROLL_SPEED * static_cast<float>(frameInfo.deltaTime);
|
||||||
if (scrollLeft) camera.position.x -= scrollDistance;
|
glm::vec3& cameraPosition =
|
||||||
if (scrollRight) camera.position.x += scrollDistance;
|
cameraEntity.getComponent<sophia::TransformComponent>().position;
|
||||||
if (scrollUp) camera.position.y -= scrollDistance;
|
if (scrollLeft) cameraPosition.x -= scrollDistance;
|
||||||
if (scrollDown) camera.position.y += 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));
|
updateBoids(static_cast<float>(frameInfo.deltaTime));
|
||||||
|
|
||||||
for (const Boid& boid : boids) {
|
for (Boid& boid : boids) {
|
||||||
drawBoid(boid);
|
syncTransform(boid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,17 +261,40 @@ class Boids2D : public sophia::Application {
|
|||||||
if (position.y < -WORLD_HALF_HEIGHT) position.y = WORLD_HALF_HEIGHT;
|
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;
|
glm::vec2 direction = boid.velocity;
|
||||||
float speed = glm::length(direction);
|
float speed = glm::length(direction);
|
||||||
direction = speed > 1e-5f ? direction / speed : glm::vec2{0.0f, -1.0f};
|
direction = speed > 1e-5f ? direction / speed : glm::vec2{0.0f, -1.0f};
|
||||||
float angle = std::atan2(direction.y, direction.x);
|
float angle = std::atan2(direction.y, direction.x);
|
||||||
|
|
||||||
glm::mat4 transform =
|
auto& transform = boid.entity.getComponent<sophia::TransformComponent>();
|
||||||
glm::translate(glm::mat4{1.f}, glm::vec3(boid.position, 0.0f)) *
|
transform.position = glm::vec3(boid.position, 0.0f);
|
||||||
glm::rotate(glm::mat4{1.f}, angle, glm::vec3(0.0f, 0.0f, 1.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) {
|
void setScrollKeyHeld(sophia::KeyCode keycode, bool held) {
|
||||||
@@ -235,7 +307,10 @@ class Boids2D : public sophia::Application {
|
|||||||
std::vector<Boid> boids;
|
std::vector<Boid> boids;
|
||||||
std::vector<std::vector<uint32_t>> grid{
|
std::vector<std::vector<uint32_t>> grid{
|
||||||
static_cast<size_t>(GRID_WIDTH * GRID_HEIGHT)};
|
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 scrollLeft = false;
|
||||||
bool scrollRight = false;
|
bool scrollRight = false;
|
||||||
bool scrollUp = false;
|
bool scrollUp = false;
|
||||||
|
|||||||
@@ -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 |
@@ -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
@@ -27,11 +27,12 @@ class Testbed : public sophia::Application {
|
|||||||
Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {}
|
Testbed() : Application{{.width = 750, .height = 1000, .name = "Testbed"}} {}
|
||||||
|
|
||||||
void onCreate() override {
|
void onCreate() override {
|
||||||
view = views().create({.scene = scenes().create(),
|
view = sceneViews().create(
|
||||||
|
{.scene = scenes().create(),
|
||||||
.target = resources().create<sophia::RenderTarget>(),
|
.target = resources().create<sophia::RenderTarget>(),
|
||||||
.clearColor = {0.1f, 0.1f, 0.15f, 1.f}});
|
.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:
|
private:
|
||||||
|
|||||||
Vendored
+17
@@ -17,6 +17,11 @@ target_link_libraries(vulkan INTERFACE
|
|||||||
Vulkan::shaderc_combined
|
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)
|
# 3. GLFW (static)
|
||||||
option(GLFW_BUILD_EXAMPLES "Build the GLFW example programs" OFF)
|
option(GLFW_BUILD_EXAMPLES "Build the GLFW example programs" OFF)
|
||||||
option(GLFW_BUILD_TESTS "Build the GLFW test 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)
|
add_library(entt INTERFACE)
|
||||||
target_include_directories(entt INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/entt)
|
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)
|
# 5. IMGUI (static)
|
||||||
set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/imgui)
|
set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/imgui)
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
#define STB_IMAGE_IMPLEMENTATION
|
||||||
|
#include "stb_image.h"
|
||||||
Vendored
+7988
File diff suppressed because it is too large
Load Diff
+1
Submodule third_party/tracy added at 30997d5ca6
Reference in New Issue
Block a user