Compare commits
1 Commits
main
..
e93b046489
| Author | SHA1 | Date | |
|---|---|---|---|
| e93b046489 |
@@ -1,8 +0,0 @@
|
|||||||
third_party
|
|
||||||
build
|
|
||||||
__dump
|
|
||||||
traces
|
|
||||||
.vscode
|
|
||||||
.claude
|
|
||||||
.git
|
|
||||||
imgui.ini
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ""
|
|
||||||
labels: ""
|
|
||||||
assignees: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
**Desktop (please complete the following information):**
|
|
||||||
|
|
||||||
- OS: [e.g. iOS]
|
|
||||||
- Browser [e.g. chrome, safari]
|
|
||||||
- Version [e.g. 22]
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
|
|
||||||
1. Something
|
|
||||||
2. ...
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ""
|
|
||||||
labels: ""
|
|
||||||
assignees: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
Relates to:
|
|
||||||
|
|
||||||
- TODO
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Implementation
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Additional resources
|
|
||||||
|
|
||||||
TODO
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
resolves TODO
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Implementation
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Testing Instructions
|
|
||||||
|
|
||||||
1. Build engine and run editor via `bin/dev editor`
|
|
||||||
+1
-7
@@ -24,6 +24,7 @@ CMakeUserPresets.json
|
|||||||
*.bin
|
*.bin
|
||||||
*.out
|
*.out
|
||||||
*.spv
|
*.spv
|
||||||
|
*.spv.h
|
||||||
|
|
||||||
# Runtime data
|
# Runtime data
|
||||||
pids
|
pids
|
||||||
@@ -32,13 +33,6 @@ 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,9 +8,3 @@
|
|||||||
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
|
|
||||||
[submodule "third_party/ImGuizmo"]
|
|
||||||
path = third_party/ImGuizmo
|
|
||||||
url = https://github.com/CedricGuillemet/ImGuizmo.git
|
|
||||||
|
|||||||
Vendored
+2
-2
@@ -41,8 +41,8 @@
|
|||||||
"LD_LIBRARY_PATH": "/home/caleb/.local/share/vulkan-sdk/1.4.357.1/x86_64/lib/VulkanLoader/lib:/home/caleb/.local/share/vulkan-sdk/1.4.357.1/x86_64/lib",
|
"LD_LIBRARY_PATH": "/home/caleb/.local/share/vulkan-sdk/1.4.357.1/x86_64/lib/VulkanLoader/lib:/home/caleb/.local/share/vulkan-sdk/1.4.357.1/x86_64/lib",
|
||||||
"VK_ADD_LAYER_PATH": "/home/caleb/.local/share/vulkan-sdk/1.4.357.1/x86_64/share/vulkan/explicit_layer.d"
|
"VK_ADD_LAYER_PATH": "/home/caleb/.local/share/vulkan-sdk/1.4.357.1/x86_64/share/vulkan/explicit_layer.d"
|
||||||
},
|
},
|
||||||
"cmake.useCMakePresets": "always",
|
"cmake.buildDirectory": "${workspaceFolder}/build",
|
||||||
"cmake.copyCompileCommands": "${workspaceFolder}/build/compile_commands.json",
|
"cmake.generator": "Ninja",
|
||||||
"cmake.configureOnOpen": true,
|
"cmake.configureOnOpen": true,
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
".clang-format": "yaml",
|
".clang-format": "yaml",
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
# Sophia
|
|
||||||
|
|
||||||
## Dev helper (`bin/dev`)
|
|
||||||
|
|
||||||
`bin/dev` wraps common project tasks. Run `bin/dev bash-completions` to list commands.
|
|
||||||
|
|
||||||
### Creating issues
|
|
||||||
|
|
||||||
Use `bin/dev issue` to create Gitea issues instead of calling the API or `tea` directly:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
bin/dev issue "<title>" [-b "<body>"] [-l <label>]... [-a <user>]... [--open]
|
|
||||||
```
|
|
||||||
|
|
||||||
- Labels are repo label names (`bug`, `enhancement`, `question`, ...).
|
|
||||||
- The Gitea API cannot add an issue to a project. After creating an issue, tell the user its URL so they can add it to the "sophia" project (issue sidebar > Projects), or pass `--open`.
|
|
||||||
- Only create issues when the user asks; issues are visible on the remote.
|
|
||||||
+4
-4
@@ -1,11 +1,11 @@
|
|||||||
cmake_minimum_required(VERSION 3.25.2)
|
cmake_minimum_required(VERSION 3.22.2)
|
||||||
|
|
||||||
set(NAME sophia)
|
set(NAME sophia)
|
||||||
|
|
||||||
project(${NAME} VERSION 0.0.0)
|
project(${NAME} VERSION 0.0.0)
|
||||||
|
|
||||||
enable_language(CXX)
|
enable_language(CXX)
|
||||||
set(CMAKE_CXX_STANDARD 23)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ endif()
|
|||||||
|
|
||||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||||
message(STATUS "Debug build: enabling debug logging")
|
message(STATUS "Debug build: enabling debug logging")
|
||||||
|
else()
|
||||||
|
add_compile_definitions(NDEBUG)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||||
@@ -37,7 +39,5 @@ endif()
|
|||||||
|
|
||||||
add_subdirectory(third_party)
|
add_subdirectory(third_party)
|
||||||
add_subdirectory(engine)
|
add_subdirectory(engine)
|
||||||
add_subdirectory(editor)
|
|
||||||
add_subdirectory(testbed)
|
add_subdirectory(testbed)
|
||||||
add_subdirectory(sandbox)
|
|
||||||
add_subdirectory(examples)
|
add_subdirectory(examples)
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 6,
|
|
||||||
"configurePresets": [
|
|
||||||
{
|
|
||||||
"name": "base",
|
|
||||||
"hidden": true,
|
|
||||||
"generator": "Ninja",
|
|
||||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
|
||||||
"cacheVariables": {
|
|
||||||
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "debug",
|
|
||||||
"displayName": "Debug",
|
|
||||||
"inherits": "base",
|
|
||||||
"cacheVariables": {
|
|
||||||
"CMAKE_BUILD_TYPE": "Debug"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "release",
|
|
||||||
"displayName": "Release (with debug info)",
|
|
||||||
"inherits": "base",
|
|
||||||
"cacheVariables": {
|
|
||||||
"CMAKE_BUILD_TYPE": "RelWithDebInfo"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "asan",
|
|
||||||
"displayName": "Debug + AddressSanitizer/UBSan",
|
|
||||||
"inherits": "debug",
|
|
||||||
"cacheVariables": {
|
|
||||||
"CMAKE_CXX_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer",
|
|
||||||
"CMAKE_C_FLAGS": "-fsanitize=address,undefined -fno-omit-frame-pointer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tsan",
|
|
||||||
"displayName": "Debug + ThreadSanitizer",
|
|
||||||
"inherits": "debug",
|
|
||||||
"cacheVariables": {
|
|
||||||
"CMAKE_CXX_FLAGS": "-fsanitize=thread",
|
|
||||||
"CMAKE_C_FLAGS": "-fsanitize=thread"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"buildPresets": [
|
|
||||||
{
|
|
||||||
"name": "debug",
|
|
||||||
"configurePreset": "debug"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "release",
|
|
||||||
"configurePreset": "release"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "asan",
|
|
||||||
"configurePreset": "asan"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "tsan",
|
|
||||||
"configurePreset": "tsan"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
# sophia
|
# sophia
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
COMING SOON
|
COMING SOON
|
||||||
@@ -10,21 +8,17 @@ COMING SOON
|
|||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
- [cmake](https://cmake.org/) 3.25+
|
- [cmake](https://cmake.org/)
|
||||||
- [Ninja](https://ninja-build.org/)
|
|
||||||
- [Vulkan SDK](https://vulkan.lunarg.com/) - With validation layers
|
- [Vulkan SDK](https://vulkan.lunarg.com/) - With validation layers
|
||||||
|
|
||||||
### Build instructions
|
### Build instructions
|
||||||
|
|
||||||
Builds use the presets in `CMakePresets.json` (`debug`, `release`), each in its own `build/<preset>` directory:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cmake --preset debug
|
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
|
||||||
cmake --build --preset debug
|
cd build
|
||||||
|
make -j
|
||||||
```
|
```
|
||||||
|
|
||||||
Or with the dev helper: `dev build [preset] [--fresh]`.
|
|
||||||
|
|
||||||
## Third Party libraries used
|
## Third Party libraries used
|
||||||
|
|
||||||
- [GLFW](https://github.com/glfw/glfw) - Window and input handling
|
- [GLFW](https://github.com/glfw/glfw) - Window and input handling
|
||||||
|
|||||||
@@ -1,56 +1,36 @@
|
|||||||
# Glossary
|
# Glossary
|
||||||
|
|
||||||
## ResourceRegistry
|
## AssetRegistry
|
||||||
|
|
||||||
A ResourceRegistry owns GPU-backed resources on the caller's behalf and hands out
|
An AssetRegistry owns GPU-backed assets on the caller's behalf and hands out
|
||||||
lightweight Handles instead of pointers or ownership. It is engine-internal:
|
lightweight Handles instead of pointers or ownership.
|
||||||
frontend code never sees it, and reaches it through a ResourceRegistryRef.
|
|
||||||
|
|
||||||
Internally it's a collection of per-type pools, each a `Pool<T, Handle>`
|
Internally it's a collection of per-type pools, each responsible for one
|
||||||
(a generation-checked slot pool). A type that needs extra behaviour wraps its
|
asset type:
|
||||||
pool in a thin class instead:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
ResourceRegistry
|
AssetRegistry
|
||||||
├── meshPool Pool<Mesh, MeshHandle>
|
├── MeshPool → MeshHandle
|
||||||
├── renderTargetPool RenderTargetPool → RenderTargetHandle
|
├── TexturePool → TextureHandle (planned)
|
||||||
│ (wraps Pool; waits for the GPU on release, applies resizes)
|
└── MaterialPool → MaterialHandle (planned)
|
||||||
├── texturePool (planned)
|
|
||||||
└── materialPool (planned)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Engine code (`Application`, `ImGuiRenderer`, `SceneRenderer`) uses the pools
|
|
||||||
directly, e.g. `resources.renderTargetPool.get(handle)`.
|
|
||||||
|
|
||||||
A Handle is a small (index, generation) pair. The index locates the slot in
|
A Handle is a small (index, generation) pair. The index locates the slot in
|
||||||
the pool; the generation is bumped when that slot is released, so a Handle
|
the pool; the generation is bumped when that slot is released, so a Handle
|
||||||
kept around after release() is detected as stale instead of resolving to
|
kept around after release() is detected as stale instead of resolving to
|
||||||
someone else's resource (or freed memory).
|
someone else's asset (or freed memory).
|
||||||
|
|
||||||
Think: "Where does the engine keep every GPU resource, and how does it look one
|
|
||||||
up by handle?"
|
|
||||||
|
|
||||||
## ResourceRegistryRef
|
|
||||||
|
|
||||||
A ResourceRegistryRef is the frontend's non-owning, cheap-to-copy view of the
|
|
||||||
ResourceRegistry, returned by `Application::resources()`. It exposes only what
|
|
||||||
frontend code needs and hands out no pools, so no Vulkan-facing types leak
|
|
||||||
through the public API:
|
|
||||||
|
|
||||||
```
|
|
||||||
create<T>(...) load<T>(path) release<T>(handle)
|
|
||||||
requestResize(target, w, h) getExtent(target) isAlive(handle)
|
|
||||||
```
|
|
||||||
|
|
||||||
Typical lifecycle:
|
Typical lifecycle:
|
||||||
|
|
||||||
```
|
```
|
||||||
load<Mesh>(path) / create<Mesh>(...) → MeshHandle
|
load<Mesh>(path) / loadMesh(...) → MeshHandle
|
||||||
|
│
|
||||||
|
get<Mesh>(handle) → const Mesh&
|
||||||
│
|
│
|
||||||
release<Mesh>(handle)
|
release<Mesh>(handle)
|
||||||
```
|
```
|
||||||
|
|
||||||
Think: "How do I create or release a GPU resource by handle, without owning its
|
Think: "Where do I get or release a GPU asset by handle, without owning its
|
||||||
lifetime directly?"
|
lifetime directly?"
|
||||||
|
|
||||||
## Buffer
|
## Buffer
|
||||||
@@ -276,13 +256,11 @@ This is primarily a UI concept, rather than a GPU concept.
|
|||||||
A PanelRegistry owns Panels on the caller's behalf and hands out lightweight
|
A PanelRegistry owns Panels on the caller's behalf and hands out lightweight
|
||||||
PanelHandles instead of pointers or ownership.
|
PanelHandles instead of pointers or ownership.
|
||||||
|
|
||||||
Internally it wraps a `Pool<Panel, PanelHandle>` (the same generation-checked
|
Internally it's a generation-checked slot pool, the same shape AssetRegistry's
|
||||||
slot pool ResourceRegistry uses, in `sophia/util/pool.hpp`) and adds the
|
per-type pools use:
|
||||||
Panel-specific parts: lookup by type or name, and the default dock layout:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
PanelRegistry
|
PanelRegistry
|
||||||
└── Pool<Panel, PanelHandle>
|
|
||||||
└── Slot { Panel, generation } → PanelHandle
|
└── Slot { Panel, generation } → PanelHandle
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -301,34 +279,6 @@ Each frame, update(frameInfo) calls onUpdate on every visible Panel it holds.
|
|||||||
Think: "Where do I register or look up a UI panel by handle, without owning
|
Think: "Where do I register or look up a UI panel by handle, without owning
|
||||||
its lifetime directly?"
|
its lifetime directly?"
|
||||||
|
|
||||||
## SceneRegistry
|
|
||||||
|
|
||||||
A SceneRegistry owns Scenes on the caller's behalf and hands out lightweight
|
|
||||||
SceneHandles instead of pointers or ownership. Like PanelRegistry, it is owned
|
|
||||||
by Application and reached from frontend code directly (`scenes()`), rather than
|
|
||||||
through ResourceRegistryRef: scenes are CPU-side entity data, not GPU resources.
|
|
||||||
|
|
||||||
Internally it wraps a `Pool<Scene, SceneHandle>`:
|
|
||||||
|
|
||||||
```
|
|
||||||
SceneRegistry
|
|
||||||
└── Pool<Scene, SceneHandle>
|
|
||||||
└── Slot { Scene, generation } → SceneHandle
|
|
||||||
```
|
|
||||||
|
|
||||||
Typical lifecycle:
|
|
||||||
|
|
||||||
```
|
|
||||||
create() → SceneHandle
|
|
||||||
│
|
|
||||||
get(handle) → Scene&
|
|
||||||
│
|
|
||||||
release(handle)
|
|
||||||
```
|
|
||||||
|
|
||||||
Think: "Where do I create or look up a Scene by handle, without owning its
|
|
||||||
lifetime directly?"
|
|
||||||
|
|
||||||
## Sprite
|
## Sprite
|
||||||
|
|
||||||
A Sprite is a 2D renderable representation of an image, usually a rectangular region of a texture.
|
A Sprite is a 2D renderable representation of an image, usually a rectangular region of a texture.
|
||||||
@@ -1,28 +1,21 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import shlex
|
import re
|
||||||
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",
|
|
||||||
"bash-completions": "bash_completions",
|
"bash-completions": "bash_completions",
|
||||||
}
|
}
|
||||||
METHOD_TO_COMMAND = {method: command for command, method in COMMAND_TO_METHOD.items()}
|
METHOD_TO_COMMAND = {method: command for command, method in COMMAND_TO_METHOD.items()}
|
||||||
|
|
||||||
WAIT_FOR_PROCESS = "wait_for_process"
|
WAIT_FOR_PROCESS = "wait_for_process"
|
||||||
|
|
||||||
class DevCommand(GiteaCommand, RenderDocCommand, ShadersCommand, TracyCommand):
|
class DevHelper:
|
||||||
PROJECT_NAME = "sophia"
|
SHADER_EXTENSIONS = (".vert", ".frag", ".comp", ".geom", ".tesc", ".tese")
|
||||||
DEFAULT_PRESET = "debug"
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.last_exit_status = 0
|
self.last_exit_status = 0
|
||||||
@@ -30,9 +23,6 @@ class DevCommand(GiteaCommand, RenderDocCommand, ShadersCommand, TracyCommand):
|
|||||||
# Core logic
|
# Core logic
|
||||||
def call(self, *args, **kwargs):
|
def call(self, *args, **kwargs):
|
||||||
if not args:
|
if not args:
|
||||||
print("Usage: dev <command> [args...]\n\nCommands:")
|
|
||||||
for command in self._commands():
|
|
||||||
print(f" {command}")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
command, rest = args[0], args[1:]
|
command, rest = args[0], args[1:]
|
||||||
@@ -42,67 +32,37 @@ class DevCommand(GiteaCommand, RenderDocCommand, ShadersCommand, TracyCommand):
|
|||||||
return method(*rest, **kwargs)
|
return method(*rest, **kwargs)
|
||||||
|
|
||||||
# Primary command wrappers
|
# Primary command wrappers
|
||||||
|
|
||||||
##
|
|
||||||
# Configures and builds a CMake preset (see CMakePresets.json).
|
|
||||||
# dev build [preset] [--fresh]
|
|
||||||
# The preset defaults to debug and builds into build/<preset>. --fresh
|
|
||||||
# discards the preset's CMake cache before configuring.
|
|
||||||
def build(self, *args, **kwargs):
|
def build(self, *args, **kwargs):
|
||||||
preset, fresh = self._parse_preset_args(args, "build")
|
command = f"rm -rf build && cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug"
|
||||||
self._build_preset(preset, fresh)
|
self._exec_in_root(command, replace=True)
|
||||||
|
|
||||||
##
|
def renderdoc(self, *args, **kwargs):
|
||||||
# Builds the debug preset (incrementally), then runs the editor from
|
qrenderdoc = self._find_qrenderdoc()
|
||||||
# build/debug. Any arguments are passed through to the editor.
|
self._exec_in_root(qrenderdoc, replace=True)
|
||||||
# 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)
|
def shaders(self, *args, **kwargs):
|
||||||
binary = os.path.join(preset_dir, "bin", "editor")
|
shaders_dir = os.path.join(self._project_root(), "engine", "src", "shaders")
|
||||||
print(f"Running: {shlex.join([binary, *args])}")
|
if not os.path.isdir(shaders_dir):
|
||||||
os.chdir(preset_dir)
|
raise ValueError(f"Shaders directory not found: {shaders_dir}")
|
||||||
os.execv(binary, [binary, *args])
|
|
||||||
|
|
||||||
##
|
glslc = self._find_glslc()
|
||||||
# Builds the debug preset (incrementally), then runs the testbed from
|
|
||||||
# build/debug. Any arguments are passed through to the testbed.
|
|
||||||
# dev testbed [args...]
|
|
||||||
def testbed(self, *args, **kwargs):
|
|
||||||
preset = self.DEFAULT_PRESET
|
|
||||||
self._build_preset(preset, fresh=False)
|
|
||||||
|
|
||||||
preset_dir = os.path.join(self._project_root(), "build", preset)
|
shader_paths = sorted(
|
||||||
binary = os.path.join(preset_dir, "bin", "testbed")
|
os.path.join(root, filename)
|
||||||
print(f"Running: {shlex.join([binary, *args])}")
|
for root, _dirs, files in os.walk(shaders_dir)
|
||||||
os.chdir(preset_dir)
|
for filename in files
|
||||||
os.execv(binary, [binary, *args])
|
if filename.endswith(self.SHADER_EXTENSIONS)
|
||||||
|
)
|
||||||
|
|
||||||
##
|
if not shader_paths:
|
||||||
# Builds the debug preset (incrementally), then runs the raw Vulkan
|
print(f"No shaders found in {shaders_dir}")
|
||||||
# sandbox from build/debug. Any arguments are passed through.
|
return
|
||||||
# dev sandbox [args...]
|
|
||||||
def sandbox(self, *args, **kwargs):
|
|
||||||
preset = self.DEFAULT_PRESET
|
|
||||||
self._build_preset(preset, fresh=False)
|
|
||||||
|
|
||||||
preset_dir = os.path.join(self._project_root(), "build", preset)
|
for shader_path in shader_paths:
|
||||||
binary = os.path.join(preset_dir, "bin", "sandbox")
|
self._generate_shader_header(glslc, shader_path)
|
||||||
print(f"Running: {shlex.join([binary, *args])}")
|
|
||||||
os.chdir(preset_dir)
|
|
||||||
os.execv(binary, [binary, *args])
|
|
||||||
|
|
||||||
# Custom helpers
|
# Custom helpers
|
||||||
|
|
||||||
##
|
|
||||||
# Counts lines of code from the project root, skipping paths listed in
|
|
||||||
# .clocignore. Any arguments are passed through to cloc.
|
|
||||||
# dev cloc [args...]
|
|
||||||
def cloc(self, *args, **kwargs):
|
|
||||||
self._run_in_root(["cloc", "--exclude-list-file=.clocignore", *args, "."])
|
|
||||||
|
|
||||||
def ownit(self, *args, **kwargs):
|
def ownit(self, *args, **kwargs):
|
||||||
file_or_directory = args[0] if args else None
|
file_or_directory = args[0] if args else None
|
||||||
if file_or_directory is None:
|
if file_or_directory is None:
|
||||||
@@ -116,61 +76,83 @@ class DevCommand(GiteaCommand, RenderDocCommand, ShadersCommand, TracyCommand):
|
|||||||
raise NotImplementedError(f"Not implemented for platform {sys.platform}")
|
raise NotImplementedError(f"Not implemented for platform {sys.platform}")
|
||||||
|
|
||||||
def bash_completions(self, *args, **kwargs):
|
def bash_completions(self, *args, **kwargs):
|
||||||
print(" ".join(self._commands()))
|
completions = sorted(
|
||||||
|
METHOD_TO_COMMAND.get(name, name)
|
||||||
|
for name in vars(DevHelper)
|
||||||
|
if not name.startswith("_") and name != "call" and callable(getattr(DevHelper, name))
|
||||||
|
)
|
||||||
|
print(" ".join(completions))
|
||||||
|
|
||||||
# Private helpers
|
# Private helpers
|
||||||
def _commands(self):
|
def _find_glslc(self):
|
||||||
return sorted(
|
glslc = shutil.which("glslc")
|
||||||
METHOD_TO_COMMAND.get(name, name)
|
if glslc is None:
|
||||||
for name in dir(DevCommand)
|
raise RuntimeError(
|
||||||
if not name.startswith("_") and name != "call" and callable(getattr(DevCommand, name))
|
"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()}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _parse_preset_args(self, args, command):
|
spirv = result.stdout
|
||||||
preset = None
|
if len(spirv) % 4 != 0:
|
||||||
fresh = False
|
raise RuntimeError(
|
||||||
for arg in args:
|
f"invalid SPIR-V output for {shader_path} (size not a multiple of 4)"
|
||||||
if arg == "--fresh":
|
)
|
||||||
fresh = True
|
|
||||||
elif arg.startswith("-"):
|
|
||||||
raise ValueError(f"Unknown option: {arg}")
|
|
||||||
elif preset is None:
|
|
||||||
preset = arg
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Usage: dev {command} [preset] [--fresh]")
|
|
||||||
|
|
||||||
preset = preset or self.DEFAULT_PRESET
|
word_count = len(spirv) // 4
|
||||||
presets = self._configure_presets()
|
words = struct.unpack(f"<{word_count}I", spirv)
|
||||||
if preset not in presets:
|
|
||||||
raise ValueError(f"Unknown preset: {preset} (available: {', '.join(presets)})")
|
|
||||||
return preset, fresh
|
|
||||||
|
|
||||||
def _configure_presets(self):
|
filename = os.path.basename(shader_path)
|
||||||
with open(os.path.join(self._project_root(), "CMakePresets.json")) as f:
|
variable_name = f"{filename.replace('.', '_')}_spv"
|
||||||
presets = json.load(f)["configurePresets"]
|
header_path = shader_path + ".spv.h"
|
||||||
return [preset["name"] for preset in presets if not preset.get("hidden")]
|
|
||||||
|
|
||||||
def _build_preset(self, preset, fresh):
|
body_lines = [
|
||||||
configure = ["cmake", "--preset", preset]
|
" " + ", ".join(f"0x{word:08x}" for word in words[i:i + 4]) + ","
|
||||||
if fresh:
|
for i in range(0, len(words), 4)
|
||||||
configure.append("--fresh")
|
]
|
||||||
self._run_in_root(configure)
|
|
||||||
self._copy_compile_commands(preset)
|
|
||||||
self._run_in_root(["cmake", "--build", "--preset", preset])
|
|
||||||
|
|
||||||
# Editor tooling reads build/compile_commands.json, so point it at the
|
header = "\n".join([
|
||||||
# most recently configured preset.
|
"#pragma once",
|
||||||
def _copy_compile_commands(self, preset):
|
"",
|
||||||
build_dir = os.path.join(self._project_root(), "build")
|
"#include <array>",
|
||||||
source = os.path.join(build_dir, preset, "compile_commands.json")
|
"#include <cstdint>",
|
||||||
if os.path.isfile(source):
|
"",
|
||||||
shutil.copyfile(source, os.path.join(build_dir, "compile_commands.json"))
|
"namespace sophia::shaders {",
|
||||||
|
"",
|
||||||
|
f"inline constexpr std::array<uint32_t, {word_count}> {variable_name} = {{",
|
||||||
|
*body_lines,
|
||||||
|
"};",
|
||||||
|
"",
|
||||||
|
"} // namespace sophia::shaders",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
|
||||||
def _run_in_root(self, command):
|
with open(header_path, "w") as f:
|
||||||
print(f"Running: {shlex.join(command)}")
|
f.write(header)
|
||||||
result = subprocess.run(command, cwd=self._project_root())
|
|
||||||
if result.returncode != 0:
|
print(f"Wrote {os.path.relpath(header_path, self._project_root())} "
|
||||||
sys.exit(result.returncode)
|
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)
|
||||||
@@ -215,4 +197,4 @@ class DevCommand(GiteaCommand, RenderDocCommand, ShadersCommand, TracyCommand):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
DevCommand().call(*sys.argv[1:])
|
DevHelper().call(*sys.argv[1:])
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
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)")
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
set(NAME editor)
|
|
||||||
set(ENGINE_NAME ${PROJECT_NAME})
|
|
||||||
|
|
||||||
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS *.cpp src/*.cpp)
|
|
||||||
|
|
||||||
add_library(${NAME}_deps INTERFACE)
|
|
||||||
|
|
||||||
target_link_libraries(${NAME}_deps INTERFACE
|
|
||||||
${ENGINE_NAME}
|
|
||||||
imguizmo
|
|
||||||
)
|
|
||||||
|
|
||||||
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23)
|
|
||||||
set_property(TARGET ${PROJECT_NAME} PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/build")
|
|
||||||
|
|
||||||
add_executable(editor ${SOURCES})
|
|
||||||
|
|
||||||
target_link_libraries(editor PRIVATE ${NAME}_deps)
|
|
||||||
|
|
||||||
target_include_directories(editor PRIVATE
|
|
||||||
"${CMAKE_SOURCE_DIR}/engine/include"
|
|
||||||
"${CMAKE_SOURCE_DIR}/external/imgui"
|
|
||||||
)
|
|
||||||
|
|
||||||
target_compile_definitions(editor PRIVATE
|
|
||||||
EDITOR_SHADER_DIR="${CMAKE_CURRENT_SOURCE_DIR}/shaders"
|
|
||||||
)
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
#include "editor_camera.hpp"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
constexpr float ORBIT_RADIANS_PER_PIXEL = 0.005f;
|
|
||||||
const float MAX_PITCH = glm::radians(89.f);
|
|
||||||
// Each wheel step moves this fraction of the way towards the pivot.
|
|
||||||
constexpr float ZOOM_STEP = 0.1f;
|
|
||||||
constexpr float MIN_DISTANCE = 0.05f;
|
|
||||||
constexpr float MIN_ORTHO_HEIGHT = 0.01f;
|
|
||||||
// Fly speed per unit of distance from the pivot, so it suits any scale.
|
|
||||||
constexpr float FLY_SPEED = 1.5f;
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void EditorCamera::orbit(glm::vec2 pixels) {
|
|
||||||
if (mode != Mode::Scene3D) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
yaw -= pixels.x * ORBIT_RADIANS_PER_PIXEL;
|
|
||||||
pitch = std::clamp(pitch - pixels.y * ORBIT_RADIANS_PER_PIXEL, -MAX_PITCH,
|
|
||||||
MAX_PITCH);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EditorCamera::pan(glm::vec2 pixels, float viewportHeight) {
|
|
||||||
if (viewportHeight <= 0.f) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// World units one pixel covers at the pivot's depth.
|
|
||||||
float unitsPerPixel =
|
|
||||||
mode == Mode::Scene3D
|
|
||||||
? 2.f * distance * std::tan(fov / 2.f) / viewportHeight
|
|
||||||
: orthoHeight / viewportHeight;
|
|
||||||
|
|
||||||
// Screen y runs down, world y up. Dragging moves the scene with the mouse,
|
|
||||||
// so the camera moves the other way.
|
|
||||||
glm::vec3 offset{-pixels.x * unitsPerPixel, pixels.y * unitsPerPixel, 0.f};
|
|
||||||
pivot += mode == Mode::Scene3D ? getRotation() * offset : offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
void EditorCamera::zoom(float steps) {
|
|
||||||
float scale = std::pow(1.f - ZOOM_STEP, steps);
|
|
||||||
if (mode == Mode::Scene3D) {
|
|
||||||
distance = std::max(distance * scale, MIN_DISTANCE);
|
|
||||||
} else {
|
|
||||||
orthoHeight = std::max(orthoHeight * scale, MIN_ORTHO_HEIGHT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void EditorCamera::fly(glm::vec3 direction, float deltaTime) {
|
|
||||||
if (mode != Mode::Scene3D || direction == glm::vec3{0.f}) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
float speed = FLY_SPEED * std::max(distance, 1.f);
|
|
||||||
pivot += getRotation() * glm::normalize(direction) * speed * deltaTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
void EditorCamera::frame(glm::vec3 target, float radius) {
|
|
||||||
pivot = target;
|
|
||||||
radius = std::max(radius, 0.1f);
|
|
||||||
// Far enough that the sphere fits in the vertical field of view.
|
|
||||||
distance = radius / std::sin(fov / 2.f);
|
|
||||||
orthoHeight = radius * 2.5f;
|
|
||||||
}
|
|
||||||
|
|
||||||
sophia::TransformComponent EditorCamera::getTransform() const {
|
|
||||||
sophia::TransformComponent transform;
|
|
||||||
if (mode == Mode::Scene3D) {
|
|
||||||
transform.rotation = getRotation();
|
|
||||||
transform.position =
|
|
||||||
pivot + transform.rotation * glm::vec3{0.f, 0.f, distance};
|
|
||||||
} else {
|
|
||||||
transform.position = pivot;
|
|
||||||
}
|
|
||||||
return transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
sophia::CameraComponent EditorCamera::getCamera() const {
|
|
||||||
sophia::CameraComponent camera;
|
|
||||||
if (mode == Mode::Scene3D) {
|
|
||||||
camera.projection = sophia::CameraComponent::Projection::Perspective;
|
|
||||||
camera.perspective.fov = fov;
|
|
||||||
// Room to zoom far out without the scene clipping.
|
|
||||||
camera.perspective.farPlane = 1000.f;
|
|
||||||
} else {
|
|
||||||
camera.projection = sophia::CameraComponent::Projection::Orthographic;
|
|
||||||
camera.ortho.height = orthoHeight;
|
|
||||||
}
|
|
||||||
return camera;
|
|
||||||
}
|
|
||||||
|
|
||||||
glm::quat EditorCamera::getRotation() const {
|
|
||||||
return glm::angleAxis(yaw, glm::vec3{0.f, 1.f, 0.f}) *
|
|
||||||
glm::angleAxis(pitch, glm::vec3{1.f, 0.f, 0.f});
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <sophia/components.hpp>
|
|
||||||
#include <sophia/math.hpp>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Scene panel's camera. Not part of any scene the editor edits, and never
|
|
||||||
* saved with one.
|
|
||||||
*
|
|
||||||
* In 3D it orbits a pivot point at some distance; in 2D it looks straight
|
|
||||||
* down -Z at the pivot through an orthographic projection, like a 2D game
|
|
||||||
* camera.
|
|
||||||
*/
|
|
||||||
class EditorCamera {
|
|
||||||
public:
|
|
||||||
enum class Mode { Scene3D, Scene2D };
|
|
||||||
|
|
||||||
Mode mode = Mode::Scene3D;
|
|
||||||
|
|
||||||
// Turns around the pivot by a mouse movement in pixels. 3D only.
|
|
||||||
void orbit(glm::vec2 pixels);
|
|
||||||
// Moves the pivot so the scene follows a mouse movement in pixels across a
|
|
||||||
// viewport `viewportHeight` pixels tall.
|
|
||||||
void pan(glm::vec2 pixels, float viewportHeight);
|
|
||||||
// Moves in (positive) or out by mouse wheel steps.
|
|
||||||
void zoom(float steps);
|
|
||||||
// Moves the pivot along the camera's axes: x right, y up, z backwards.
|
|
||||||
// `direction` needn't be normalized. 3D only.
|
|
||||||
void fly(glm::vec3 direction, float deltaTime);
|
|
||||||
// Centers on `target`, backing off enough to see a sphere of `radius`.
|
|
||||||
void frame(glm::vec3 target, float radius);
|
|
||||||
|
|
||||||
sophia::TransformComponent getTransform() const;
|
|
||||||
sophia::CameraComponent getCamera() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
glm::quat getRotation() const;
|
|
||||||
|
|
||||||
glm::vec3 pivot{0.f, 0.f, 0.f};
|
|
||||||
|
|
||||||
// 3D: looking at the pivot from in front, a little to the right and above.
|
|
||||||
float yaw = glm::radians(30.f);
|
|
||||||
float pitch = glm::radians(-25.f);
|
|
||||||
float distance = 8.f;
|
|
||||||
float fov = glm::radians(60.f);
|
|
||||||
|
|
||||||
// 2D: world units visible from the bottom of the view to the top.
|
|
||||||
float orthoHeight = 10.f;
|
|
||||||
};
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <sophia/entity.hpp>
|
|
||||||
#include <sophia/handles.hpp>
|
|
||||||
#include <sophia/scene.hpp>
|
|
||||||
#include <sophia/scene_registry.hpp>
|
|
||||||
#include <sophia/scene_view_registry.hpp>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What the editor is working on, shared by its panels. Panels read it every
|
|
||||||
* frame instead of holding on to a scene, so switching scenes is one call.
|
|
||||||
*/
|
|
||||||
class EditorContext {
|
|
||||||
public:
|
|
||||||
// A scene the editor has open, and can switch to.
|
|
||||||
struct OpenScene {
|
|
||||||
sophia::SceneHandle handle;
|
|
||||||
std::string name;
|
|
||||||
};
|
|
||||||
|
|
||||||
explicit EditorContext(sophia::SceneRegistry& scenes) : scenes{scenes} {}
|
|
||||||
|
|
||||||
// Creates an open scene with a "Main Camera", without switching to it.
|
|
||||||
sophia::SceneHandle createScene(std::string name) {
|
|
||||||
sophia::SceneHandle handle = scenes.create();
|
|
||||||
scenes.get(handle).createCamera("Main Camera");
|
|
||||||
openScenes.push_back({handle, std::move(name)});
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::vector<OpenScene>& getOpenScenes() const { return openScenes; }
|
|
||||||
|
|
||||||
// Null if `handle` isn't an open scene.
|
|
||||||
const std::string* getSceneName(sophia::SceneHandle handle) const {
|
|
||||||
for (const OpenScene& open : openScenes) {
|
|
||||||
if (open.handle == handle) {
|
|
||||||
return &open.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void renameScene(sophia::SceneHandle handle, std::string name) {
|
|
||||||
for (OpenScene& open : openScenes) {
|
|
||||||
if (open.handle == handle) {
|
|
||||||
open.name = std::move(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hasScene() const { return scenes.isAlive(activeScene); }
|
|
||||||
sophia::SceneHandle getSceneHandle() const { return activeScene; }
|
|
||||||
sophia::Scene& getScene() { return scenes.get(activeScene); }
|
|
||||||
|
|
||||||
// Also clears the selection, which belonged to the previous scene.
|
|
||||||
void setScene(sophia::SceneHandle handle) {
|
|
||||||
activeScene = handle;
|
|
||||||
selected = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Null when nothing is selected. Always an entity of the active scene.
|
|
||||||
sophia::Entity selected;
|
|
||||||
|
|
||||||
private:
|
|
||||||
sophia::SceneRegistry& scenes;
|
|
||||||
sophia::SceneHandle activeScene = sophia::INVALID_HANDLE;
|
|
||||||
// Kept open even when not active: with no saving yet, closing one would
|
|
||||||
// lose it.
|
|
||||||
std::vector<OpenScene> openScenes;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Points `view` at the context's active scene, and renders it only while
|
|
||||||
// there is one and its panel is `shown`. Call every frame from the panel that
|
|
||||||
// owns the view.
|
|
||||||
inline void syncSceneView(EditorContext& context,
|
|
||||||
sophia::SceneViewRegistry& sceneViews,
|
|
||||||
sophia::SceneViewHandle view, bool shown) {
|
|
||||||
bool enabled = shown && context.hasScene();
|
|
||||||
const sophia::SceneView& current = sceneViews.peek(view);
|
|
||||||
// get() re-sorts the render queue, so only use it when something changed.
|
|
||||||
if (current.enabled != enabled ||
|
|
||||||
(context.hasScene() && current.scene != context.getSceneHandle())) {
|
|
||||||
sophia::SceneView& updated = sceneViews.get(view);
|
|
||||||
updated.enabled = enabled;
|
|
||||||
updated.scene = context.getSceneHandle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-153
@@ -1,153 +0,0 @@
|
|||||||
#include <sophia/application.hpp>
|
|
||||||
#include <sophia/components.hpp>
|
|
||||||
#include <sophia/entity.hpp>
|
|
||||||
#include <sophia/primitives.hpp>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "editor_context.hpp"
|
|
||||||
#include "panels/asset_browser_panel.hpp"
|
|
||||||
#include "panels/game_panel.hpp"
|
|
||||||
#include "panels/inspector_panel.hpp"
|
|
||||||
#include "panels/scene_hierarchy_panel.hpp"
|
|
||||||
#include "panels/scene_panel.hpp"
|
|
||||||
#include "utilities/logger.hpp"
|
|
||||||
|
|
||||||
class Editor : public sophia::Application {
|
|
||||||
public:
|
|
||||||
Editor() : Application{{.width = 750, .height = 1000, .name = "Editor"}} {}
|
|
||||||
|
|
||||||
void onCreate() override {
|
|
||||||
log("Editor created");
|
|
||||||
|
|
||||||
debugUI().show();
|
|
||||||
|
|
||||||
textures.emplace_back("Checker", createCheckerTexture());
|
|
||||||
cubeMesh = createShadedCube();
|
|
||||||
|
|
||||||
sophia::SceneHandle sceneHandle = context.createScene("Demo");
|
|
||||||
createDemoScene(scenes().get(sceneHandle));
|
|
||||||
context.setScene(sceneHandle);
|
|
||||||
|
|
||||||
// The editor's own scene, never edited or rendered. It holds the camera
|
|
||||||
// the Scene panel looks through, which a SceneView may take from any
|
|
||||||
// scene.
|
|
||||||
sophia::SceneHandle editorSceneHandle = scenes().create();
|
|
||||||
sophia::Entity editorCamera =
|
|
||||||
scenes().get(editorSceneHandle).createCamera("Editor Camera");
|
|
||||||
|
|
||||||
// Views start without a scene; their panels point them at the active
|
|
||||||
// scene each frame.
|
|
||||||
sophia::RenderTargetHandle sceneTarget =
|
|
||||||
resources().create<sophia::RenderTarget>();
|
|
||||||
sophia::SceneViewHandle sceneView = sceneViews().create(
|
|
||||||
{.target = sceneTarget, .camera = editorCamera, .enabled = false});
|
|
||||||
|
|
||||||
// No camera: the Game panel shows the scene through its active one.
|
|
||||||
sophia::RenderTargetHandle gameTarget =
|
|
||||||
resources().create<sophia::RenderTarget>();
|
|
||||||
sophia::SceneViewHandle gameView =
|
|
||||||
sceneViews().create({.target = gameTarget, .enabled = false});
|
|
||||||
|
|
||||||
panels().emplace<SceneHierarchyPanel>(context, cubeMesh);
|
|
||||||
panels().emplace<InspectorPanel>(context, textures);
|
|
||||||
panels().emplace<AssetBrowserPanel>();
|
|
||||||
viewPanels.push_back(
|
|
||||||
{panels().emplace<ScenePanel>(ui(), resources(), context, sceneViews(),
|
|
||||||
sceneView, sceneTarget, editorCamera),
|
|
||||||
sceneView});
|
|
||||||
viewPanels.push_back(
|
|
||||||
{panels().emplace<GamePanel>(ui(), context, sceneViews(), gameView,
|
|
||||||
gameTarget),
|
|
||||||
gameView});
|
|
||||||
|
|
||||||
panels().setDefaultLayout(
|
|
||||||
{.left = {"Scene Hierarchy"},
|
|
||||||
.right = {"Inspector", "Frame Stats", "PanelRegistry"},
|
|
||||||
.bottom = {"Asset Browser", "Console"},
|
|
||||||
.center = {"Scene", "Game"}});
|
|
||||||
}
|
|
||||||
|
|
||||||
void onUpdate(const sophia::FrameInfo& frameInfo) override {
|
|
||||||
// A closed panel's onUpdate() doesn't run, so it can't stop its view
|
|
||||||
// rendering itself.
|
|
||||||
for (const ViewPanel& viewPanel : viewPanels) {
|
|
||||||
if (!panels().get(viewPanel.panel).isVisible() &&
|
|
||||||
sceneViews().peek(viewPanel.view).enabled) {
|
|
||||||
sceneViews().get(viewPanel.view).enabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
// Adds to a new scene, which already has a camera.
|
|
||||||
void createDemoScene(sophia::Scene& scene) {
|
|
||||||
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::Entity cube = scene.createEntity("Cube");
|
|
||||||
auto& cubeTransform = cube.getComponent<sophia::TransformComponent>();
|
|
||||||
cubeTransform.position = {0.f, 2.5f, 0.f};
|
|
||||||
cubeTransform.rotation =
|
|
||||||
glm::quat{glm::radians(glm::vec3{30.f, 45.f, 0.f})};
|
|
||||||
cube.addComponent<sophia::MeshComponent>(sophia::MeshComponent{
|
|
||||||
.handle = cubeMesh, .color = {0.4f, 0.8f, 0.5f, 1.f}});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Meshes are unlit, so a plain cube is a flat silhouette. Bake a fixed
|
|
||||||
// light into its vertex colors so its faces read in 3D until there's real
|
|
||||||
// lighting; MeshComponent::color still tints it.
|
|
||||||
sophia::MeshHandle createShadedCube() {
|
|
||||||
const glm::vec3 lightDirection = glm::normalize(glm::vec3{0.4f, 1.f, 0.7f});
|
|
||||||
sophia::MeshData data = sophia::primitives::cube();
|
|
||||||
for (sophia::MeshVertex& vertex : data.vertices) {
|
|
||||||
float light = glm::dot(vertex.normal, lightDirection) * 0.5f + 0.5f;
|
|
||||||
vertex.color = glm::vec4{glm::vec3{0.35f + 0.65f * light}, 1.f};
|
|
||||||
}
|
|
||||||
return resources().create<sophia::Mesh>(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
// A panel showing a scene view, which should only render while the panel
|
|
||||||
// is on screen.
|
|
||||||
struct ViewPanel {
|
|
||||||
sophia::PanelHandle panel;
|
|
||||||
sophia::SceneViewHandle view;
|
|
||||||
};
|
|
||||||
|
|
||||||
EditorContext context{scenes()};
|
|
||||||
std::vector<ViewPanel> viewPanels;
|
|
||||||
InspectorPanel::TextureList textures;
|
|
||||||
sophia::MeshHandle cubeMesh;
|
|
||||||
};
|
|
||||||
|
|
||||||
SOPHIA_MAIN(Editor)
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
#include "asset_browser_panel.hpp"
|
|
||||||
|
|
||||||
#include <imgui.h>
|
|
||||||
|
|
||||||
void AssetBrowserPanel::onUpdate(const sophia::FrameInfo&) {
|
|
||||||
ImGui::BeginChild("Folders", ImVec2(150, 0), true);
|
|
||||||
for (int i = 0; i < static_cast<int>(folders.size()); i++) {
|
|
||||||
if (ImGui::Selectable(folders[static_cast<size_t>(i)].name.c_str(),
|
|
||||||
selectedFolder == i)) {
|
|
||||||
selectedFolder = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::EndChild();
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
|
||||||
|
|
||||||
ImGui::BeginChild("Files", ImVec2(0, 0), true);
|
|
||||||
const Folder& folder = folders[static_cast<size_t>(selectedFolder)];
|
|
||||||
for (const std::string& file : folder.files) {
|
|
||||||
ImGui::Selectable(file.c_str());
|
|
||||||
}
|
|
||||||
ImGui::EndChild();
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <sophia/frame_info.hpp>
|
|
||||||
#include <sophia/ui/panel.hpp>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class AssetBrowserPanel : public sophia::Panel {
|
|
||||||
public:
|
|
||||||
AssetBrowserPanel() : Panel{{.name = "Asset Browser"}} {}
|
|
||||||
|
|
||||||
void onUpdate(const sophia::FrameInfo& frameInfo) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct Folder {
|
|
||||||
std::string name;
|
|
||||||
std::vector<std::string> files;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::vector<Folder> folders = {
|
|
||||||
{"Models", {"player_capsule.fbx", "ground_plane.fbx", "enemy_grunt.fbx"}},
|
|
||||||
{"Textures",
|
|
||||||
{"brick_diffuse.png", "grass_albedo.png", "player_diffuse.png"}},
|
|
||||||
{"Materials", {"player_mat.mat", "ground_mat.mat"}},
|
|
||||||
{"Scripts", {"PlayerController.lua", "EnemyAI.lua"}},
|
|
||||||
{"Scenes", {"MainMenu.scene", "Level_01.scene"}},
|
|
||||||
};
|
|
||||||
|
|
||||||
int selectedFolder = 0;
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
#include "game_panel.hpp"
|
|
||||||
|
|
||||||
#include <imgui.h>
|
|
||||||
|
|
||||||
void GamePanel::onUpdate(const sophia::FrameInfo&) {
|
|
||||||
syncSceneView(context, sceneViews, view, isShown());
|
|
||||||
|
|
||||||
if (!context.hasScene()) {
|
|
||||||
ImGui::TextDisabled("No scene open");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
|
||||||
ui.image(target);
|
|
||||||
|
|
||||||
if (!context.getScene().getActiveCamera()) {
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(origin.x + 8.0f, origin.y + 8.0f));
|
|
||||||
ImGui::TextDisabled("The scene has no active camera");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <sophia/frame_info.hpp>
|
|
||||||
#include <sophia/handles.hpp>
|
|
||||||
#include <sophia/scene_view_registry.hpp>
|
|
||||||
#include <sophia/ui/panel.hpp>
|
|
||||||
#include <sophia/ui/ui.hpp>
|
|
||||||
|
|
||||||
#include "../editor_context.hpp"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the active scene through its own active camera: what the game will
|
|
||||||
* see.
|
|
||||||
*/
|
|
||||||
class GamePanel : public sophia::Panel {
|
|
||||||
public:
|
|
||||||
// Renders into `target` through `view`, a SceneView with no camera of its
|
|
||||||
// own that this panel keeps pointed at the active scene.
|
|
||||||
GamePanel(sophia::UI& ui, EditorContext& context,
|
|
||||||
sophia::SceneViewRegistry& sceneViews, sophia::SceneViewHandle view,
|
|
||||||
sophia::RenderTargetHandle target)
|
|
||||||
: Panel{{.name = "Game",
|
|
||||||
.windowFlags = ImGuiWindowFlags_NoScrollbar |
|
|
||||||
ImGuiWindowFlags_NoScrollWithMouse}},
|
|
||||||
ui{ui},
|
|
||||||
context{context},
|
|
||||||
sceneViews{sceneViews},
|
|
||||||
view{view},
|
|
||||||
target{target} {}
|
|
||||||
|
|
||||||
void onUpdate(const sophia::FrameInfo& frameInfo) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
sophia::UI& ui;
|
|
||||||
EditorContext& context;
|
|
||||||
sophia::SceneViewRegistry& sceneViews;
|
|
||||||
sophia::SceneViewHandle view;
|
|
||||||
sophia::RenderTargetHandle target;
|
|
||||||
};
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
#include "inspector_panel.hpp"
|
|
||||||
|
|
||||||
#include <imgui.h>
|
|
||||||
|
|
||||||
#include <cstdio>
|
|
||||||
#include <sophia/components.hpp>
|
|
||||||
|
|
||||||
void InspectorPanel::onUpdate(const sophia::FrameInfo&) {
|
|
||||||
selected = context.selected;
|
|
||||||
if (!selected) {
|
|
||||||
ImGui::TextDisabled("No entity selected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
drawTag();
|
|
||||||
ImGui::Separator();
|
|
||||||
drawTransform();
|
|
||||||
drawCamera();
|
|
||||||
drawSprite();
|
|
||||||
drawMesh();
|
|
||||||
|
|
||||||
ImGui::Separator();
|
|
||||||
drawAddComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
void InspectorPanel::drawTag() {
|
|
||||||
auto& tag = selected.getComponent<sophia::TagComponent>();
|
|
||||||
|
|
||||||
char nameBuf[128];
|
|
||||||
std::snprintf(nameBuf, sizeof(nameBuf), "%s", tag.tag.c_str());
|
|
||||||
if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) {
|
|
||||||
tag.tag = nameBuf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void InspectorPanel::drawTransform() {
|
|
||||||
if (!selected.hasComponent<sophia::TransformComponent>() ||
|
|
||||||
!ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto& transform = selected.getComponent<sophia::TransformComponent>();
|
|
||||||
ImGui::DragFloat3("Position", &transform.position.x, 0.1f);
|
|
||||||
|
|
||||||
// Euler angles in degrees, recomputed from the quaternion every frame.
|
|
||||||
// Near 90 degrees of pitch the angles can jump between equivalent values
|
|
||||||
// while dragging.
|
|
||||||
// TODO: keep each entity's edited angles, or replace with gizmo rotation.
|
|
||||||
glm::vec3 degrees = glm::degrees(glm::eulerAngles(transform.rotation));
|
|
||||||
if (ImGui::DragFloat3("Rotation", °rees.x, 1.0f)) {
|
|
||||||
transform.rotation = glm::quat{glm::radians(degrees)};
|
|
||||||
}
|
|
||||||
|
|
||||||
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::drawMesh() {
|
|
||||||
if (!selected.hasComponent<sophia::MeshComponent>()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool open = true;
|
|
||||||
if (!ImGui::CollapsingHeader("Mesh", &open, ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
||||||
if (!open) {
|
|
||||||
selected.removeComponent<sophia::MeshComponent>();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto& mesh = selected.getComponent<sophia::MeshComponent>();
|
|
||||||
// TODO: pick the mesh once assets have names.
|
|
||||||
ImGui::Text("Mesh: %u", static_cast<sophia::u32>(mesh.handle.index));
|
|
||||||
ImGui::ColorEdit3("Color", &mesh.color.x);
|
|
||||||
|
|
||||||
if (!open) {
|
|
||||||
selected.removeComponent<sophia::MeshComponent>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,36 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <sophia/entity.hpp>
|
|
||||||
#include <sophia/frame_info.hpp>
|
|
||||||
#include <sophia/handles.hpp>
|
|
||||||
#include <sophia/ui/panel.hpp>
|
|
||||||
#include <string>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "../editor_context.hpp"
|
|
||||||
|
|
||||||
class InspectorPanel : public sophia::Panel {
|
|
||||||
public:
|
|
||||||
// Stand-in for an asset browser: the textures a sprite can pick from.
|
|
||||||
using TextureList =
|
|
||||||
std::vector<std::pair<std::string, sophia::TextureHandle>>;
|
|
||||||
|
|
||||||
InspectorPanel(EditorContext& context, const TextureList& textures)
|
|
||||||
: Panel{{.name = "Inspector"}}, context{context}, textures{textures} {}
|
|
||||||
|
|
||||||
void onUpdate(const sophia::FrameInfo& frameInfo) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void drawTag();
|
|
||||||
void drawTransform();
|
|
||||||
void drawCamera();
|
|
||||||
void drawSprite();
|
|
||||||
void drawMesh();
|
|
||||||
void drawAddComponent();
|
|
||||||
|
|
||||||
EditorContext& context;
|
|
||||||
// The selected entity, for the duration of onUpdate().
|
|
||||||
sophia::Entity selected;
|
|
||||||
const TextureList& textures;
|
|
||||||
};
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
#include "scene_hierarchy_panel.hpp"
|
|
||||||
|
|
||||||
#include <imgui.h>
|
|
||||||
|
|
||||||
#include <cstdio>
|
|
||||||
#include <sophia/components.hpp>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
void SceneHierarchyPanel::onUpdate(const sophia::FrameInfo&) {
|
|
||||||
drawSceneSelector();
|
|
||||||
ImGui::Separator();
|
|
||||||
|
|
||||||
if (!context.hasScene()) {
|
|
||||||
ImGui::TextDisabled("No scene open");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sophia::Scene& scene = context.getScene();
|
|
||||||
sophia::Entity& selected = context.selected;
|
|
||||||
|
|
||||||
if (ImGui::Button("+ Sprite")) {
|
|
||||||
selected = scene.createEntity("Sprite");
|
|
||||||
selected.addComponent<sophia::SpriteComponent>();
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
if (ImGui::Button("+ Cube")) {
|
|
||||||
selected = scene.createEntity("Cube");
|
|
||||||
selected.addComponent<sophia::MeshComponent>(
|
|
||||||
sophia::MeshComponent{.handle = cubeMesh});
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Separator();
|
|
||||||
|
|
||||||
// Flat for now: there is no parent/child component yet.
|
|
||||||
scene.forEachEntity([&selected](sophia::Entity entity) {
|
|
||||||
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf |
|
|
||||||
ImGuiTreeNodeFlags_NoTreePushOnOpen |
|
|
||||||
ImGuiTreeNodeFlags_SpanAvailWidth;
|
|
||||||
if (entity == selected) {
|
|
||||||
flags |= ImGuiTreeNodeFlags_Selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& name = entity.getComponent<sophia::TagComponent>().tag;
|
|
||||||
ImGui::PushID(static_cast<int>(static_cast<sophia::u32>(entity)));
|
|
||||||
ImGui::TreeNodeEx(name.c_str(), flags);
|
|
||||||
if (ImGui::IsItemClicked()) {
|
|
||||||
selected = entity;
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
// See SceneHierarchyPanel::renameFocusFrames.
|
|
||||||
constexpr int RENAME_FOCUS_FRAMES = 2;
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void SceneHierarchyPanel::drawSceneSelector() {
|
|
||||||
// Leave room for the button: its label, its padding, and the gap to it.
|
|
||||||
constexpr const char* ADD_SCENE_LABEL = "Add Scene";
|
|
||||||
const ImGuiStyle& style = ImGui::GetStyle();
|
|
||||||
ImGui::SetNextItemWidth(-(ImGui::CalcTextSize(ADD_SCENE_LABEL).x +
|
|
||||||
style.FramePadding.x * 2.f + style.ItemSpacing.x));
|
|
||||||
|
|
||||||
const std::string* current = context.getSceneName(context.getSceneHandle());
|
|
||||||
bool startRename = false;
|
|
||||||
|
|
||||||
if (renamingScene && current) {
|
|
||||||
drawSceneRename();
|
|
||||||
} else {
|
|
||||||
renamingScene = false;
|
|
||||||
if (ImGui::BeginCombo("##scene", current ? current->c_str() : "No scene")) {
|
|
||||||
for (const EditorContext::OpenScene& open : context.getOpenScenes()) {
|
|
||||||
ImGui::PushID(static_cast<int>(open.handle.index));
|
|
||||||
bool isCurrent = open.handle == context.getSceneHandle();
|
|
||||||
if (ImGui::Selectable(open.name.c_str(), isCurrent) && !isCurrent) {
|
|
||||||
context.setScene(open.handle);
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
}
|
|
||||||
ImGui::EndCombo();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current && ImGui::BeginPopupContextItem("##sceneMenu")) {
|
|
||||||
startRename = ImGui::MenuItem("Rename", "F2");
|
|
||||||
ImGui::EndPopup();
|
|
||||||
}
|
|
||||||
startRename = startRename || (current && ImGui::IsWindowFocused() &&
|
|
||||||
!ImGui::GetIO().WantTextInput &&
|
|
||||||
ImGui::IsKeyPressed(ImGuiKey_F2));
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
|
||||||
if (ImGui::Button(ADD_SCENE_LABEL)) {
|
|
||||||
std::string name = "Untitled " + std::to_string(++this->untitledSceneCount);
|
|
||||||
context.setScene(context.createScene(std::move(name)));
|
|
||||||
startRename = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startRename) {
|
|
||||||
renamingScene = true;
|
|
||||||
renameFocusFrames = RENAME_FOCUS_FRAMES;
|
|
||||||
std::snprintf(renameBuffer.data(), renameBuffer.size(), "%s",
|
|
||||||
current->c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SceneHierarchyPanel::drawSceneRename() {
|
|
||||||
if (renameFocusFrames == RENAME_FOCUS_FRAMES) {
|
|
||||||
ImGui::SetKeyboardFocusHere();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::InputText("##sceneName", renameBuffer.data(), renameBuffer.size(),
|
|
||||||
ImGuiInputTextFlags_AutoSelectAll);
|
|
||||||
|
|
||||||
bool active = ImGui::IsItemActive();
|
|
||||||
if (active) {
|
|
||||||
renameFocusFrames = 0;
|
|
||||||
} else if (renameFocusFrames > 0) {
|
|
||||||
renameFocusFrames--;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Escape reverts the text before deactivating, so committing whatever is
|
|
||||||
// left when the field closes covers Enter, clicking away and Escape alike.
|
|
||||||
if (!active) {
|
|
||||||
std::string name = renameBuffer.data();
|
|
||||||
// Keep the old name rather than leave the scene nameless.
|
|
||||||
if (name.find_first_not_of(' ') != std::string::npos) {
|
|
||||||
context.renameScene(context.getSceneHandle(), std::move(name));
|
|
||||||
}
|
|
||||||
renamingScene = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <sophia/frame_info.hpp>
|
|
||||||
#include <sophia/handles.hpp>
|
|
||||||
#include <sophia/ui/panel.hpp>
|
|
||||||
|
|
||||||
#include "../editor_context.hpp"
|
|
||||||
|
|
||||||
class SceneHierarchyPanel : public sophia::Panel {
|
|
||||||
public:
|
|
||||||
// `cubeMesh` is what "+ Cube" gives new entities.
|
|
||||||
SceneHierarchyPanel(EditorContext& context, sophia::MeshHandle cubeMesh)
|
|
||||||
: Panel{{.name = "Scene Hierarchy", .locked = true}},
|
|
||||||
context{context},
|
|
||||||
cubeMesh{cubeMesh} {}
|
|
||||||
|
|
||||||
void onUpdate(const sophia::FrameInfo& frameInfo) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
// Switches between open scenes, renames the active one, and creates new
|
|
||||||
// ones.
|
|
||||||
void drawSceneSelector();
|
|
||||||
// Replaces the selector with a text field editing the active scene's name.
|
|
||||||
void drawSceneRename();
|
|
||||||
|
|
||||||
EditorContext& context;
|
|
||||||
sophia::MeshHandle cubeMesh;
|
|
||||||
// For naming new scenes "Untitled 1", "Untitled 2", ...
|
|
||||||
int untitledSceneCount = 0;
|
|
||||||
|
|
||||||
// Set while the scene selector is a text field for renaming.
|
|
||||||
bool renamingScene = false;
|
|
||||||
// Frames the text field has left to become active after renaming starts;
|
|
||||||
// focus can take a frame to land. If it never does, renaming ends.
|
|
||||||
int renameFocusFrames = 0;
|
|
||||||
std::array<char, 128> renameBuffer{};
|
|
||||||
};
|
|
||||||
@@ -1,603 +0,0 @@
|
|||||||
#include "scene_panel.hpp"
|
|
||||||
|
|
||||||
#include <imgui.h>
|
|
||||||
// After imgui.h, which it relies on.
|
|
||||||
#include <ImGuizmo.h>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
|
||||||
#include <cmath>
|
|
||||||
#include <glm/gtc/type_ptr.hpp>
|
|
||||||
#include <limits>
|
|
||||||
#include <optional>
|
|
||||||
#include <sophia/aabb.hpp>
|
|
||||||
#include <sophia/components.hpp>
|
|
||||||
#include <tuple>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Holding Ctrl snaps the gizmo to these steps.
|
|
||||||
constexpr float TRANSLATE_SNAP = 0.5f;
|
|
||||||
constexpr float ROTATE_SNAP_DEGREES = 15.f;
|
|
||||||
constexpr float SCALE_SNAP = 0.1f;
|
|
||||||
|
|
||||||
// Writes a matrix built from a translation, rotation and scale (no shear)
|
|
||||||
// back into `transform`. A mirroring matrix comes back with a negative x
|
|
||||||
// scale.
|
|
||||||
void decompose(const glm::mat4& matrix, sophia::TransformComponent& transform) {
|
|
||||||
transform.position = glm::vec3{matrix[3]};
|
|
||||||
|
|
||||||
glm::vec3 scale{glm::length(glm::vec3{matrix[0]}),
|
|
||||||
glm::length(glm::vec3{matrix[1]}),
|
|
||||||
glm::length(glm::vec3{matrix[2]})};
|
|
||||||
if (glm::determinant(glm::mat3{matrix}) < 0.f) {
|
|
||||||
scale.x = -scale.x;
|
|
||||||
}
|
|
||||||
transform.scale = scale;
|
|
||||||
|
|
||||||
// A zero scale loses the axis, and with it the rotation; keep the old one.
|
|
||||||
if (scale.x == 0.f || scale.y == 0.f || scale.z == 0.f) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
glm::mat3 rotation{glm::vec3{matrix[0]} / scale.x,
|
|
||||||
glm::vec3{matrix[1]} / scale.y,
|
|
||||||
glm::vec3{matrix[2]} / scale.z};
|
|
||||||
transform.rotation = glm::normalize(glm::quat_cast(rotation));
|
|
||||||
}
|
|
||||||
|
|
||||||
// How far along a ray (from `origin`, along the normalized `direction`) it
|
|
||||||
// enters `bounds` placed by `model`, or null if it misses. A ray starting
|
|
||||||
// inside hits at 0.
|
|
||||||
std::optional<float> intersectBox(const glm::vec3& origin,
|
|
||||||
const glm::vec3& direction,
|
|
||||||
const glm::mat4& model,
|
|
||||||
const sophia::AABB& bounds) {
|
|
||||||
if (glm::determinant(model) == 0.f) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test in the box's space, where it's axis-aligned. The direction isn't
|
|
||||||
// renormalized, so distances along it are still world distances.
|
|
||||||
glm::mat4 inverse = glm::inverse(model);
|
|
||||||
glm::vec3 localOrigin = glm::vec3{inverse * glm::vec4{origin, 1.f}};
|
|
||||||
glm::vec3 localDirection = glm::vec3{inverse * glm::vec4{direction, 0.f}};
|
|
||||||
|
|
||||||
float enter = 0.f;
|
|
||||||
float exit = std::numeric_limits<float>::infinity();
|
|
||||||
for (int axis = 0; axis < 3; axis++) {
|
|
||||||
if (std::abs(localDirection[axis]) < 1e-8f) {
|
|
||||||
// Parallel to this pair of faces: hits only if already between them.
|
|
||||||
if (localOrigin[axis] < bounds.min[axis] ||
|
|
||||||
localOrigin[axis] > bounds.max[axis]) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
float toMin = (bounds.min[axis] - localOrigin[axis]) / localDirection[axis];
|
|
||||||
float toMax = (bounds.max[axis] - localOrigin[axis]) / localDirection[axis];
|
|
||||||
if (toMin > toMax) {
|
|
||||||
std::swap(toMin, toMax);
|
|
||||||
}
|
|
||||||
enter = std::max(enter, toMin);
|
|
||||||
exit = std::min(exit, toMax);
|
|
||||||
if (enter > exit) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return enter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// How far along a ray it hits the unit quad (0, 0)-(1, 1) in the XY plane,
|
|
||||||
// placed by `model`, or null if it misses.
|
|
||||||
std::optional<float> intersectQuad(const glm::vec3& origin,
|
|
||||||
const glm::vec3& direction,
|
|
||||||
const glm::mat4& model) {
|
|
||||||
if (glm::determinant(model) == 0.f) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
glm::mat4 inverse = glm::inverse(model);
|
|
||||||
glm::vec3 localOrigin = glm::vec3{inverse * glm::vec4{origin, 1.f}};
|
|
||||||
glm::vec3 localDirection = glm::vec3{inverse * glm::vec4{direction, 0.f}};
|
|
||||||
// Edge on to the quad.
|
|
||||||
if (std::abs(localDirection.z) < 1e-8f) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
float distance = -localOrigin.z / localDirection.z;
|
|
||||||
if (distance < 0.f) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
glm::vec3 hit = localOrigin + distance * localDirection;
|
|
||||||
if (hit.x < 0.f || hit.x > 1.f || hit.y < 0.f || hit.y > 1.f) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
return distance;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
ImVec2 ScenePanel::ViewportTransform::clipToScreen(
|
|
||||||
const glm::vec4& clip) const {
|
|
||||||
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};
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<ImVec2> ScenePanel::ViewportTransform::worldToScreen(
|
|
||||||
const glm::vec3& world) const {
|
|
||||||
glm::vec4 clip = viewProjection * glm::vec4{world, 1.f};
|
|
||||||
if (clip.w <= 0.f) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
return clipToScreen(clip);
|
|
||||||
}
|
|
||||||
|
|
||||||
glm::vec2 ScenePanel::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;
|
|
||||||
}
|
|
||||||
|
|
||||||
ScenePanel::Ray ScenePanel::ViewportTransform::screenToRay(
|
|
||||||
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};
|
|
||||||
// Depth runs 0 at the near plane to 1 at the far one.
|
|
||||||
glm::vec4 nearPoint = inverseViewProjection * glm::vec4{ndc, 0.f, 1.f};
|
|
||||||
glm::vec4 farPoint = inverseViewProjection * glm::vec4{ndc, 1.f, 1.f};
|
|
||||||
glm::vec3 from = glm::vec3{nearPoint} / nearPoint.w;
|
|
||||||
glm::vec3 to = glm::vec3{farPoint} / farPoint.w;
|
|
||||||
return {from, glm::normalize(to - from)};
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScenePanel::ViewportTransform::drawLine(ImDrawList* drawList,
|
|
||||||
const glm::vec3& a,
|
|
||||||
const glm::vec3& b, ImU32 color,
|
|
||||||
float thickness) const {
|
|
||||||
// Just in front of the camera. Points behind it project mirrored, so the
|
|
||||||
// part of the line there is cut off rather than drawn.
|
|
||||||
constexpr float MIN_W = 1e-4f;
|
|
||||||
|
|
||||||
glm::vec4 clipA = viewProjection * glm::vec4{a, 1.f};
|
|
||||||
glm::vec4 clipB = viewProjection * glm::vec4{b, 1.f};
|
|
||||||
if (clipA.w < MIN_W && clipB.w < MIN_W) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (clipA.w < MIN_W) {
|
|
||||||
clipA = glm::mix(clipA, clipB, (MIN_W - clipA.w) / (clipB.w - clipA.w));
|
|
||||||
} else if (clipB.w < MIN_W) {
|
|
||||||
clipB = glm::mix(clipB, clipA, (MIN_W - clipB.w) / (clipA.w - clipB.w));
|
|
||||||
}
|
|
||||||
drawList->AddLine(clipToScreen(clipA), clipToScreen(clipB), color, thickness);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScenePanel::onUpdate(const sophia::FrameInfo& frameInfo) {
|
|
||||||
syncSceneView(context, sceneViews, view, isShown());
|
|
||||||
|
|
||||||
drawToolbar();
|
|
||||||
ImGui::Separator();
|
|
||||||
|
|
||||||
const ImVec2 origin = ImGui::GetCursorScreenPos();
|
|
||||||
const ImVec2 size = ImGui::GetContentRegionAvail();
|
|
||||||
if (size.x < 1.0f || size.y < 1.0f) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!context.hasScene()) {
|
|
||||||
ImGui::TextDisabled("No scene open");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.image(target);
|
|
||||||
|
|
||||||
// Rolls over ImGuizmo's "mouse is over a handle" state, so IsOver() below
|
|
||||||
// reports last frame's.
|
|
||||||
ImGuizmo::BeginFrame();
|
|
||||||
// A handle can't be grabbed while any ImGui item is hovered, so while the
|
|
||||||
// mouse is over one, leave out the viewport button, and keep the window
|
|
||||||
// still so dragging the handle doesn't drag it. Unless a camera drag is
|
|
||||||
// already under way: dropping the button would end it.
|
|
||||||
bool overGizmo =
|
|
||||||
(ImGuizmo::IsOver() || ImGuizmo::IsUsing()) && !viewportActive;
|
|
||||||
getConfig().windowFlags =
|
|
||||||
overGizmo ? WINDOW_FLAGS | ImGuiWindowFlags_NoMove : WINDOW_FLAGS;
|
|
||||||
|
|
||||||
// The image doesn't take mouse input, so without this, dragging in it
|
|
||||||
// would drag the window instead.
|
|
||||||
ImGui::SetCursorScreenPos(origin);
|
|
||||||
if (overGizmo) {
|
|
||||||
ImGui::Dummy(size);
|
|
||||||
} else {
|
|
||||||
ImGui::InvisibleButton("##viewport", size,
|
|
||||||
ImGuiButtonFlags_MouseButtonLeft |
|
|
||||||
ImGuiButtonFlags_MouseButtonRight |
|
|
||||||
ImGuiButtonFlags_MouseButtonMiddle);
|
|
||||||
}
|
|
||||||
viewportActive = !overGizmo && ImGui::IsItemActive();
|
|
||||||
|
|
||||||
handleCameraInput(frameInfo, size);
|
|
||||||
handleGizmoHotkeys();
|
|
||||||
|
|
||||||
// Drive the view's camera from the editor camera. The scene view renders
|
|
||||||
// after the panels update, so this frame already uses it.
|
|
||||||
camera.getComponent<sophia::TransformComponent>() =
|
|
||||||
editorCamera.getTransform();
|
|
||||||
camera.getComponent<sophia::CameraComponent>() = editorCamera.getCamera();
|
|
||||||
|
|
||||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
||||||
drawList->PushClipRect(origin, ImVec2(origin.x + size.x, origin.y + size.y),
|
|
||||||
true);
|
|
||||||
|
|
||||||
// Screen-space, so it only lines up with anything in 2D.
|
|
||||||
// TODO: a world-space grid on the XZ plane for 3D, once there's debug
|
|
||||||
// drawing in scene views.
|
|
||||||
if (showGrid && editorCamera.mode == EditorCamera::Mode::Scene2D) {
|
|
||||||
drawGrid(origin, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 view = sophia::CameraComponent::getView(
|
|
||||||
camera.getComponent<sophia::TransformComponent>());
|
|
||||||
glm::mat4 viewProjection =
|
|
||||||
camera.getComponent<sophia::CameraComponent>().getProjection(aspect) *
|
|
||||||
view;
|
|
||||||
ViewportTransform viewport{origin, size, viewProjection,
|
|
||||||
glm::inverse(viewProjection)};
|
|
||||||
|
|
||||||
handleSelectionInput(viewport);
|
|
||||||
drawSelection(viewport);
|
|
||||||
|
|
||||||
ImGuizmo::SetDrawlist();
|
|
||||||
ImGuizmo::SetRect(origin.x, origin.y, size.x, size.y);
|
|
||||||
drawGizmo(view, aspect);
|
|
||||||
|
|
||||||
drawList->PopClipRect();
|
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(origin.x + 8.0f, origin.y + 8.0f));
|
|
||||||
ImGui::TextDisabled("%.0f x %.0f", size.x, size.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScenePanel::drawToolbar() {
|
|
||||||
bool is2D = editorCamera.mode == EditorCamera::Mode::Scene2D;
|
|
||||||
if (ImGui::Checkbox("2D", &is2D)) {
|
|
||||||
editorCamera.mode =
|
|
||||||
is2D ? EditorCamera::Mode::Scene2D : EditorCamera::Mode::Scene3D;
|
|
||||||
dragging = false;
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::BeginDisabled(!is2D);
|
|
||||||
ImGui::Checkbox("Grid", &showGrid);
|
|
||||||
ImGui::EndDisabled();
|
|
||||||
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::TextDisabled("|");
|
|
||||||
ImGui::SameLine();
|
|
||||||
const struct {
|
|
||||||
const char* label;
|
|
||||||
GizmoOperation operation;
|
|
||||||
} operations[] = {{"Move (W)", GizmoOperation::Translate},
|
|
||||||
{"Rotate (E)", GizmoOperation::Rotate},
|
|
||||||
{"Scale (R)", GizmoOperation::Scale}};
|
|
||||||
for (const auto& entry : operations) {
|
|
||||||
if (ImGui::RadioButton(entry.label, gizmoOperation == entry.operation)) {
|
|
||||||
gizmoOperation = entry.operation;
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
}
|
|
||||||
if (ImGui::Button(gizmoLocal ? "Local" : "World")) {
|
|
||||||
gizmoLocal = !gizmoLocal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScenePanel::handleGizmoHotkeys() {
|
|
||||||
const ImGuiIO& io = ImGui::GetIO();
|
|
||||||
// W/E/Q fly the camera while the right button is held.
|
|
||||||
if (!ImGui::IsWindowFocused() || io.WantTextInput ||
|
|
||||||
ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_W, false)) {
|
|
||||||
gizmoOperation = GizmoOperation::Translate;
|
|
||||||
} else if (ImGui::IsKeyPressed(ImGuiKey_E, false)) {
|
|
||||||
gizmoOperation = GizmoOperation::Rotate;
|
|
||||||
} else if (ImGui::IsKeyPressed(ImGuiKey_R, false)) {
|
|
||||||
gizmoOperation = GizmoOperation::Scale;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScenePanel::drawGizmo(const glm::mat4& view, float aspect) {
|
|
||||||
sophia::Entity selected = context.selected;
|
|
||||||
if (!selected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool is2D = editorCamera.mode == EditorCamera::Mode::Scene2D;
|
|
||||||
|
|
||||||
// In 2D, only the axes that keep things in the XY plane.
|
|
||||||
ImGuizmo::OPERATION operation{};
|
|
||||||
float snapStep = 0.f;
|
|
||||||
switch (gizmoOperation) {
|
|
||||||
case GizmoOperation::Translate:
|
|
||||||
operation = is2D ? ImGuizmo::TRANSLATE_X | ImGuizmo::TRANSLATE_Y
|
|
||||||
: ImGuizmo::TRANSLATE;
|
|
||||||
snapStep = TRANSLATE_SNAP;
|
|
||||||
break;
|
|
||||||
case GizmoOperation::Rotate:
|
|
||||||
operation = is2D ? ImGuizmo::ROTATE_Z : ImGuizmo::ROTATE;
|
|
||||||
snapStep = ROTATE_SNAP_DEGREES;
|
|
||||||
break;
|
|
||||||
case GizmoOperation::Scale:
|
|
||||||
operation =
|
|
||||||
is2D ? ImGuizmo::SCALE_X | ImGuizmo::SCALE_Y : ImGuizmo::SCALE;
|
|
||||||
snapStep = SCALE_SNAP;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
std::array<float, 3> snap{snapStep, snapStep, snapStep};
|
|
||||||
bool snapping = ImGui::GetIO().KeyCtrl;
|
|
||||||
|
|
||||||
// ImGuizmo maps clip space to the screen the OpenGL way, with +y up, so it
|
|
||||||
// needs the projection without the Y flip CameraComponent adds for Vulkan.
|
|
||||||
glm::mat4 projection =
|
|
||||||
camera.getComponent<sophia::CameraComponent>().getProjection(aspect);
|
|
||||||
projection[1][1] *= -1.f;
|
|
||||||
|
|
||||||
auto& transform = selected.getComponent<sophia::TransformComponent>();
|
|
||||||
glm::mat4 model = transform.mat4();
|
|
||||||
|
|
||||||
ImGuizmo::SetOrthographic(is2D);
|
|
||||||
// Per entity, so a drag in progress doesn't carry over to a new selection.
|
|
||||||
ImGuizmo::PushID(static_cast<int>(static_cast<sophia::u32>(selected)));
|
|
||||||
bool changed = ImGuizmo::Manipulate(
|
|
||||||
glm::value_ptr(view), glm::value_ptr(projection), operation,
|
|
||||||
gizmoLocal ? ImGuizmo::LOCAL : ImGuizmo::WORLD, glm::value_ptr(model),
|
|
||||||
nullptr, snapping ? snap.data() : nullptr);
|
|
||||||
ImGuizmo::PopID();
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
decompose(model, transform);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScenePanel::drawGrid(ImVec2 origin, ImVec2 size) {
|
|
||||||
constexpr float GRID_STEP = 40.0f;
|
|
||||||
const ImU32 gridColor = IM_COL32(255, 255, 255, 18);
|
|
||||||
const ImVec2 end(origin.x + size.x, origin.y + size.y);
|
|
||||||
|
|
||||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
||||||
for (float x = GRID_STEP; x < size.x; x += GRID_STEP) {
|
|
||||||
drawList->AddLine(ImVec2(origin.x + x, origin.y),
|
|
||||||
ImVec2(origin.x + x, end.y), gridColor);
|
|
||||||
}
|
|
||||||
for (float y = GRID_STEP; y < size.y; y += GRID_STEP) {
|
|
||||||
drawList->AddLine(ImVec2(origin.x, origin.y + y),
|
|
||||||
ImVec2(end.x, origin.y + y), gridColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScenePanel::handleCameraInput(const sophia::FrameInfo& frameInfo,
|
|
||||||
ImVec2 size) {
|
|
||||||
const ImGuiIO& io = ImGui::GetIO();
|
|
||||||
bool is3D = editorCamera.mode == EditorCamera::Mode::Scene3D;
|
|
||||||
glm::vec2 mouseDelta{io.MouseDelta.x, io.MouseDelta.y};
|
|
||||||
|
|
||||||
// The button stays active while any of its mouse buttons is held, even
|
|
||||||
// once the mouse leaves the panel.
|
|
||||||
if (ImGui::IsItemActive()) {
|
|
||||||
bool rightDown = ImGui::IsMouseDown(ImGuiMouseButton_Right);
|
|
||||||
bool middleDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle);
|
|
||||||
|
|
||||||
if (is3D && rightDown) {
|
|
||||||
editorCamera.orbit(mouseDelta);
|
|
||||||
|
|
||||||
glm::vec3 direction{0.f};
|
|
||||||
if (ImGui::IsKeyDown(ImGuiKey_D)) direction.x += 1.f;
|
|
||||||
if (ImGui::IsKeyDown(ImGuiKey_A)) direction.x -= 1.f;
|
|
||||||
if (ImGui::IsKeyDown(ImGuiKey_E)) direction.y += 1.f;
|
|
||||||
if (ImGui::IsKeyDown(ImGuiKey_Q)) direction.y -= 1.f;
|
|
||||||
if (ImGui::IsKeyDown(ImGuiKey_S)) direction.z += 1.f;
|
|
||||||
if (ImGui::IsKeyDown(ImGuiKey_W)) direction.z -= 1.f;
|
|
||||||
editorCamera.fly(direction, static_cast<float>(frameInfo.deltaTime));
|
|
||||||
} else if (middleDown || (!is3D && rightDown)) {
|
|
||||||
editorCamera.pan(mouseDelta, size.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui::IsItemHovered() && io.MouseWheel != 0.f) {
|
|
||||||
editorCamera.zoom(io.MouseWheel);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui::IsWindowFocused() && !io.WantTextInput &&
|
|
||||||
ImGui::IsKeyPressed(ImGuiKey_F, false)) {
|
|
||||||
frameSelection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScenePanel::handleSelectionInput(const ViewportTransform& viewport) {
|
|
||||||
const ImGuiIO& io = ImGui::GetIO();
|
|
||||||
|
|
||||||
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
|
|
||||||
context.selected = pick(viewport.screenToRay(io.MousePos));
|
|
||||||
// Dragging the selection around is 2D only; in 3D, use the gizmo.
|
|
||||||
dragging =
|
|
||||||
context.selected && editorCamera.mode == EditorCamera::Mode::Scene2D;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ImGui::IsItemActive() || !ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
|
||||||
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 =
|
|
||||||
context.selected.getComponent<sophia::TransformComponent>();
|
|
||||||
transform.position.x += delta.x;
|
|
||||||
transform.position.y += delta.y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScenePanel::drawSelection(const ViewportTransform& viewport) {
|
|
||||||
sophia::Entity selected = context.selected;
|
|
||||||
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);
|
|
||||||
for (size_t i = 0; i < UNIT_QUAD.size(); i++) {
|
|
||||||
const glm::vec2& from = UNIT_QUAD[i];
|
|
||||||
const glm::vec2& to = UNIT_QUAD[(i + 1) % UNIT_QUAD.size()];
|
|
||||||
viewport.drawLine(drawList, glm::vec3{model * glm::vec4{from, 0.f, 1.f}},
|
|
||||||
glm::vec3{model * glm::vec4{to, 0.f, 1.f}},
|
|
||||||
SELECTION_COLOR, 2.0f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (std::optional<sophia::AABB> bounds = getMeshBounds(selected)) {
|
|
||||||
glm::mat4 model = transform.mat4();
|
|
||||||
std::array<glm::vec3, 8> corners = bounds->getCorners();
|
|
||||||
for (glm::vec3& corner : corners) {
|
|
||||||
corner = glm::vec3{model * glm::vec4{corner, 1.f}};
|
|
||||||
}
|
|
||||||
// Corners whose indices differ in one bit share an edge.
|
|
||||||
for (int i = 0; i < 8; i++) {
|
|
||||||
for (int bit : {1, 2, 4}) {
|
|
||||||
if ((i & bit) == 0) {
|
|
||||||
viewport.drawLine(drawList, corners[i], corners[i | bit],
|
|
||||||
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 or mesh, such as the
|
|
||||||
// camera.
|
|
||||||
if (std::optional<ImVec2> center =
|
|
||||||
viewport.worldToScreen(transform.position)) {
|
|
||||||
drawList->AddCircle(*center, 4.0f, SELECTION_COLOR, 0, 2.0f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ScenePanel::frameSelection() {
|
|
||||||
sophia::Entity selected = context.selected;
|
|
||||||
if (!selected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto& transform = selected.getComponent<sophia::TransformComponent>();
|
|
||||||
|
|
||||||
// The selection's corners in world space, from its mesh or sprite.
|
|
||||||
std::vector<glm::vec3> points;
|
|
||||||
if (std::optional<sophia::AABB> bounds = getMeshBounds(selected)) {
|
|
||||||
glm::mat4 model = transform.mat4();
|
|
||||||
for (const glm::vec3& corner : bounds->getCorners()) {
|
|
||||||
points.push_back(glm::vec3{model * glm::vec4{corner, 1.f}});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (selected.hasComponent<sophia::SpriteComponent>()) {
|
|
||||||
glm::mat4 model =
|
|
||||||
selected.getComponent<sophia::SpriteComponent>().getModel(transform);
|
|
||||||
for (const glm::vec2& corner : UNIT_QUAD) {
|
|
||||||
points.push_back(glm::vec3{model * glm::vec4{corner, 0.f, 1.f}});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (points.empty()) {
|
|
||||||
// Nothing with a size: frame a unit around its position.
|
|
||||||
editorCamera.frame(transform.position, 0.5f);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
glm::vec3 min = points.front();
|
|
||||||
glm::vec3 max = points.front();
|
|
||||||
for (const glm::vec3& point : points) {
|
|
||||||
min = glm::min(min, point);
|
|
||||||
max = glm::max(max, point);
|
|
||||||
}
|
|
||||||
editorCamera.frame((min + max) * 0.5f, glm::length(max - min) * 0.5f);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<sophia::AABB> ScenePanel::getMeshBounds(sophia::Entity entity) {
|
|
||||||
if (!entity.hasComponent<sophia::MeshComponent>()) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
sophia::MeshHandle mesh = entity.getComponent<sophia::MeshComponent>().handle;
|
|
||||||
if (!resources.isAlive(mesh)) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
return resources.getBounds(mesh);
|
|
||||||
}
|
|
||||||
|
|
||||||
sophia::Entity ScenePanel::pick(const Ray& ray) {
|
|
||||||
sophia::Scene& scene = context.getScene();
|
|
||||||
|
|
||||||
// Meshes are opaque and depth tested, so the nearest one hides anything
|
|
||||||
// behind it.
|
|
||||||
sophia::Entity nearestMesh;
|
|
||||||
float meshDistance = std::numeric_limits<float>::infinity();
|
|
||||||
scene.forEachEntity([&](sophia::Entity entity) {
|
|
||||||
std::optional<sophia::AABB> bounds = getMeshBounds(entity);
|
|
||||||
if (!bounds) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
glm::mat4 model = entity.getComponent<sophia::TransformComponent>().mat4();
|
|
||||||
std::optional<float> distance =
|
|
||||||
intersectBox(ray.origin, ray.direction, model, *bounds);
|
|
||||||
if (distance && *distance < meshDistance) {
|
|
||||||
nearestMesh = entity;
|
|
||||||
meshDistance = *distance;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sprites in front of that mesh draw over it, in the renderer's order:
|
|
||||||
// higher sortOrder on top, then nearer.
|
|
||||||
sophia::Entity topSprite;
|
|
||||||
std::optional<std::tuple<sophia::s32, float>> topOrder;
|
|
||||||
scene.forEachEntity([&](sophia::Entity entity) {
|
|
||||||
if (!entity.hasComponent<sophia::SpriteComponent>()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto& sprite = entity.getComponent<sophia::SpriteComponent>();
|
|
||||||
glm::mat4 model =
|
|
||||||
sprite.getModel(entity.getComponent<sophia::TransformComponent>());
|
|
||||||
std::optional<float> distance =
|
|
||||||
intersectQuad(ray.origin, ray.direction, model);
|
|
||||||
if (!distance || *distance > meshDistance) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Negated distance, so nearer compares greater.
|
|
||||||
std::tuple order{sprite.sortOrder, -*distance};
|
|
||||||
if (!topOrder || order > *topOrder) {
|
|
||||||
topSprite = entity;
|
|
||||||
topOrder = order;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return topSprite ? topSprite : nearestMesh;
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
#include <sophia/aabb.hpp>
|
|
||||||
#include <sophia/entity.hpp>
|
|
||||||
#include <sophia/frame_info.hpp>
|
|
||||||
#include <sophia/handles.hpp>
|
|
||||||
#include <sophia/math.hpp>
|
|
||||||
#include <sophia/resource_registry_ref.hpp>
|
|
||||||
#include <sophia/scene_view_registry.hpp>
|
|
||||||
#include <sophia/ui/panel.hpp>
|
|
||||||
#include <sophia/ui/ui.hpp>
|
|
||||||
|
|
||||||
#include "../editor_camera.hpp"
|
|
||||||
#include "../editor_context.hpp"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the active scene through the editor's own camera.
|
|
||||||
*
|
|
||||||
* 3D: right-drag orbits, middle-drag pans, the wheel zooms, right-drag with
|
|
||||||
* WASD/QE flies. 2D: right- or middle-drag pans, the wheel zooms. F frames
|
|
||||||
* the selection in both.
|
|
||||||
*
|
|
||||||
* The selection gets a transform gizmo: W moves, E rotates, R scales, and
|
|
||||||
* holding Ctrl snaps. Clicking selects the mesh or sprite under the mouse;
|
|
||||||
* in 2D, dragging it also moves it.
|
|
||||||
*/
|
|
||||||
class ScenePanel : public sophia::Panel {
|
|
||||||
public:
|
|
||||||
// Renders into `target` through `view`, a SceneView this panel keeps
|
|
||||||
// pointed at the active scene. `camera` is the entity the view looks
|
|
||||||
// through; it belongs to a scene of the editor's own, and this panel
|
|
||||||
// drives it from its EditorCamera.
|
|
||||||
ScenePanel(sophia::UI& ui, sophia::ResourceRegistryRef resources,
|
|
||||||
EditorContext& context, sophia::SceneViewRegistry& sceneViews,
|
|
||||||
sophia::SceneViewHandle view, sophia::RenderTargetHandle target,
|
|
||||||
sophia::Entity camera)
|
|
||||||
: Panel{{.name = "Scene", .windowFlags = WINDOW_FLAGS}},
|
|
||||||
ui{ui},
|
|
||||||
resources{resources},
|
|
||||||
context{context},
|
|
||||||
sceneViews{sceneViews},
|
|
||||||
view{view},
|
|
||||||
target{target},
|
|
||||||
camera{camera} {}
|
|
||||||
|
|
||||||
void onUpdate(const sophia::FrameInfo& frameInfo) override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
static constexpr ImGuiWindowFlags WINDOW_FLAGS =
|
|
||||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
|
|
||||||
|
|
||||||
enum class GizmoOperation { Translate, Rotate, Scale };
|
|
||||||
|
|
||||||
// A half-line in world space. `direction` is normalized, so distances
|
|
||||||
// along it are in world units.
|
|
||||||
struct Ray {
|
|
||||||
glm::vec3 origin;
|
|
||||||
glm::vec3 direction;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
// Null for points behind the camera.
|
|
||||||
std::optional<ImVec2> worldToScreen(const glm::vec3& world) const;
|
|
||||||
// Where `screen` is on the camera's near plane. Only the x and y, which
|
|
||||||
// is all that's needed in 2D.
|
|
||||||
glm::vec2 screenToWorld(const ImVec2& screen) const;
|
|
||||||
// From the near plane through `screen`, into the scene.
|
|
||||||
Ray screenToRay(const ImVec2& screen) const;
|
|
||||||
|
|
||||||
// Draws a world-space line, cut off where it passes behind the camera.
|
|
||||||
void drawLine(ImDrawList* drawList, const glm::vec3& a, const glm::vec3& b,
|
|
||||||
ImU32 color, float thickness) const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
ImVec2 clipToScreen(const glm::vec4& clip) const;
|
|
||||||
};
|
|
||||||
|
|
||||||
void drawToolbar();
|
|
||||||
void drawGrid(ImVec2 origin, ImVec2 size);
|
|
||||||
void handleCameraInput(const sophia::FrameInfo& frameInfo, ImVec2 size);
|
|
||||||
void handleSelectionInput(const ViewportTransform& viewport);
|
|
||||||
void handleGizmoHotkeys();
|
|
||||||
// Draws the transform gizmo on the selection, and applies any change made
|
|
||||||
// with it. `view` is the editor camera's, and `aspect` the viewport's
|
|
||||||
// width over its height.
|
|
||||||
void drawGizmo(const glm::mat4& view, float aspect);
|
|
||||||
void drawSelection(const ViewportTransform& viewport);
|
|
||||||
void frameSelection();
|
|
||||||
|
|
||||||
// The bounds of the entity's mesh in the mesh's own space, if it has one.
|
|
||||||
std::optional<sophia::AABB> getMeshBounds(sophia::Entity entity);
|
|
||||||
|
|
||||||
// The entity drawn in front along `ray`, or a null Entity. Only meshes and
|
|
||||||
// sprites can be hit.
|
|
||||||
sophia::Entity pick(const Ray& ray);
|
|
||||||
|
|
||||||
sophia::UI& ui;
|
|
||||||
sophia::ResourceRegistryRef resources;
|
|
||||||
EditorContext& context;
|
|
||||||
sophia::SceneViewRegistry& sceneViews;
|
|
||||||
sophia::SceneViewHandle view;
|
|
||||||
sophia::RenderTargetHandle target;
|
|
||||||
sophia::Entity camera;
|
|
||||||
|
|
||||||
EditorCamera editorCamera;
|
|
||||||
|
|
||||||
GizmoOperation gizmoOperation = GizmoOperation::Translate;
|
|
||||||
// Whether the gizmo works along the entity's own axes or the world's.
|
|
||||||
bool gizmoLocal = true;
|
|
||||||
// Whether the viewport button was active last frame, i.e. a camera drag or
|
|
||||||
// click is under way, which the gizmo mustn't interrupt.
|
|
||||||
bool viewportActive = false;
|
|
||||||
|
|
||||||
bool showGrid = true;
|
|
||||||
// Set while a drag that started on the selected entity is moving it.
|
|
||||||
bool dragging = false;
|
|
||||||
};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <format>
|
|
||||||
#include <sophia/util/logger.hpp>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
template <typename... Args>
|
|
||||||
void log(std::format_string<Args...> fmt, Args&&... args) {
|
|
||||||
if constexpr (sophia::log::enabled) {
|
|
||||||
sophia::log::print("\033[1;38;2;170;90;255m[editor]\033[0m {}",
|
|
||||||
std::format(fmt, std::forward<Args>(args)...));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,22 +17,10 @@ target_include_directories(${ENGINE_NAME} PUBLIC
|
|||||||
"${CMAKE_CURRENT_SOURCE_DIR}/include"
|
"${CMAKE_CURRENT_SOURCE_DIR}/include"
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_definitions(${ENGINE_NAME} PUBLIC
|
|
||||||
GLM_FORCE_RADIANS
|
|
||||||
GLM_FORCE_DEPTH_ZERO_TO_ONE
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(${ENGINE_NAME} PUBLIC
|
target_link_libraries(${ENGINE_NAME} PUBLIC
|
||||||
vulkan
|
vulkan
|
||||||
vma
|
|
||||||
shaderc
|
|
||||||
glfw
|
glfw
|
||||||
glm
|
glm
|
||||||
imgui
|
imgui
|
||||||
entt
|
entt
|
||||||
Tracy::TracyClient
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(${ENGINE_NAME} PRIVATE
|
|
||||||
stb_image
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <sophia/math.hpp>
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
/** @brief Axis-Aligned Bounding Box
|
|
||||||
*
|
|
||||||
* An axis-aligned box, from its smallest corner to its largest.
|
|
||||||
*/
|
|
||||||
struct AABB {
|
|
||||||
vec3 min{0.f, 0.f, 0.f};
|
|
||||||
vec3 max{0.f, 0.f, 0.f};
|
|
||||||
|
|
||||||
vec3 getCenter() const { return (min + max) * 0.5f; }
|
|
||||||
vec3 getSize() const { return max - min; }
|
|
||||||
|
|
||||||
// The 8 corners. Bit 0 of the index picks x from max, bit 1 y, bit 2 z.
|
|
||||||
std::array<vec3, 8> getCorners() const {
|
|
||||||
std::array<vec3, 8> corners;
|
|
||||||
for (int i = 0; i < 8; i++) {
|
|
||||||
corners[i] = {(i & 1) ? max.x : min.x, (i & 2) ? max.y : min.y,
|
|
||||||
(i & 4) ? max.z : min.z};
|
|
||||||
}
|
|
||||||
return corners;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -3,20 +3,21 @@
|
|||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <sophia/asset_registry.hpp>
|
||||||
|
#include <sophia/camera_2d.hpp>
|
||||||
#include <sophia/canvas.hpp>
|
#include <sophia/canvas.hpp>
|
||||||
#include <sophia/console/console_command_registry.hpp>
|
#include <sophia/console/console_command_registry.hpp>
|
||||||
#include <sophia/frame_info.hpp>
|
#include <sophia/frame_info.hpp>
|
||||||
#include <sophia/math.hpp>
|
|
||||||
#include <sophia/resource_registry_ref.hpp>
|
|
||||||
#include <sophia/scene_registry.hpp>
|
|
||||||
#include <sophia/scene_view_registry.hpp>
|
|
||||||
#include <sophia/types.hpp>
|
#include <sophia/types.hpp>
|
||||||
#include <sophia/ui/debug_ui.hpp>
|
#include <sophia/ui/debug_ui.hpp>
|
||||||
#include <sophia/ui/panel_registry.hpp>
|
#include <sophia/ui/panel_registry.hpp>
|
||||||
#include <sophia/ui/ui.hpp>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#define GLM_FORCE_RADIANS
|
||||||
|
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
class Application {
|
class Application {
|
||||||
@@ -43,15 +44,13 @@ class Application {
|
|||||||
virtual void onRender() {}
|
virtual void onRender() {}
|
||||||
virtual void onDestroy() {}
|
virtual void onDestroy() {}
|
||||||
|
|
||||||
ResourceRegistryRef resources();
|
AssetRegistry& assets() { return this->assetRegistry; }
|
||||||
PanelRegistry& panels() { return this->panelRegistry; }
|
PanelRegistry& panels() { return this->panelRegistry; }
|
||||||
SceneRegistry& scenes() { return this->sceneRegistry; }
|
|
||||||
SceneViewRegistry& sceneViews() { return this->sceneViewRegistry; }
|
|
||||||
UI& ui() { return this->_ui; }
|
|
||||||
|
|
||||||
DebugUI& debugUI() { return this->_debugUI; }
|
DebugUI& debugUI() { return this->_debugUI; }
|
||||||
|
|
||||||
Canvas canvas;
|
Canvas canvas;
|
||||||
|
Camera2D camera;
|
||||||
std::vector<std::string> args;
|
std::vector<std::string> args;
|
||||||
|
|
||||||
ConsoleCommandRegistry consoleCommandRegistry;
|
ConsoleCommandRegistry consoleCommandRegistry;
|
||||||
@@ -67,10 +66,8 @@ class Application {
|
|||||||
class Impl;
|
class Impl;
|
||||||
std::unique_ptr<Impl> impl;
|
std::unique_ptr<Impl> impl;
|
||||||
|
|
||||||
|
AssetRegistry assetRegistry;
|
||||||
PanelRegistry panelRegistry;
|
PanelRegistry panelRegistry;
|
||||||
SceneRegistry sceneRegistry;
|
|
||||||
SceneViewRegistry sceneViewRegistry;
|
|
||||||
UI _ui;
|
|
||||||
|
|
||||||
DebugUI _debugUI;
|
DebugUI _debugUI;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <sophia/handles.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "sophia/canvas.hpp"
|
||||||
|
#include "sophia/types.hpp"
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
class Device;
|
||||||
|
class Mesh;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
struct AssetTraits;
|
||||||
|
|
||||||
|
template <>
|
||||||
|
struct AssetTraits<Mesh> {
|
||||||
|
using Handle = MeshHandle;
|
||||||
|
};
|
||||||
|
|
||||||
|
class AssetRegistry {
|
||||||
|
public:
|
||||||
|
~AssetRegistry();
|
||||||
|
|
||||||
|
AssetRegistry(const AssetRegistry&) = delete;
|
||||||
|
AssetRegistry& operator=(const AssetRegistry&) = delete;
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
typename AssetTraits<T>::Handle load(const std::string& path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TEMPORARY: Remove once load<Mesh> can read real model files from disk.
|
||||||
|
*/
|
||||||
|
MeshHandle loadMesh(std::vector<CanvasVertex> vertices,
|
||||||
|
std::vector<u32> indices);
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void release(typename AssetTraits<T>::Handle handle);
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class Application;
|
||||||
|
|
||||||
|
explicit AssetRegistry(Device& device);
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
const T& get(typename AssetTraits<T>::Handle handle) const;
|
||||||
|
|
||||||
|
class Impl;
|
||||||
|
std::unique_ptr<Impl> impl;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <>
|
||||||
|
MeshHandle AssetRegistry::load<Mesh>(const std::string& path);
|
||||||
|
|
||||||
|
template <>
|
||||||
|
void AssetRegistry::release<Mesh>(MeshHandle handle);
|
||||||
|
|
||||||
|
template <>
|
||||||
|
const Mesh& AssetRegistry::get<Mesh>(MeshHandle handle) const;
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define GLM_FORCE_RADIANS
|
||||||
|
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
class Camera2D {
|
||||||
|
public:
|
||||||
|
glm::vec2 position{0.f, 0.f};
|
||||||
|
|
||||||
|
void setProjection(float left, float right, float bottom, float top) {
|
||||||
|
projection = glm::ortho(left, right, bottom, top);
|
||||||
|
}
|
||||||
|
|
||||||
|
const glm::mat4& getProjection() const { return projection; }
|
||||||
|
|
||||||
|
glm::mat4 getView() const {
|
||||||
|
return glm::translate(glm::mat4(1.f), glm::vec3(-position, 0.f));
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::mat4 getViewProjection() const { return projection * getView(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
glm::mat4 projection{1.f};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -2,9 +2,12 @@
|
|||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <sophia/frame_info.hpp>
|
#include <sophia/frame_info.hpp>
|
||||||
#include <sophia/math.hpp>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#define GLM_FORCE_RADIANS
|
||||||
|
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
class Application;
|
class Application;
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <glm/gtc/matrix_transform.hpp>
|
#include <sophia/asset_registry.hpp>
|
||||||
#include <iostream>
|
|
||||||
#include <ostream>
|
|
||||||
#include <sophia/handles.hpp>
|
|
||||||
#include <sophia/math.hpp>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#define GLM_FORCE_RADIANS
|
||||||
|
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
struct TagComponent {
|
struct TagComponent {
|
||||||
std::string tag; // entt::hashed_string ?
|
std::string tag; // entt::hashed_string ?
|
||||||
uint32_t mask = 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct TransformComponent {
|
struct TransformComponent {
|
||||||
@@ -27,117 +28,10 @@ struct TransformComponent {
|
|||||||
glm::mat3 normalMatrix() const {
|
glm::mat3 normalMatrix() const {
|
||||||
return glm::mat3{glm::transpose(glm::inverse(mat4()))};
|
return glm::mat3{glm::transpose(glm::inverse(mat4()))};
|
||||||
}
|
}
|
||||||
|
|
||||||
friend std::ostream& operator<<(std::ostream& os,
|
|
||||||
const TransformComponent& transform) {
|
|
||||||
os << "TransformComponent {\n"
|
|
||||||
<< " Position: (" << transform.position.x << ", "
|
|
||||||
<< transform.position.y << ", " << transform.position.z << ")\n"
|
|
||||||
<< " Rotation (w, x, y, z): (" << transform.rotation.w << ", "
|
|
||||||
<< transform.rotation.x << ", " << transform.rotation.y << ", "
|
|
||||||
<< transform.rotation.z << ")\n"
|
|
||||||
<< " Scale: (" << transform.scale.x << ", " << transform.scale.y << ", "
|
|
||||||
<< transform.scale.z << ")\n"
|
|
||||||
<< "}";
|
|
||||||
return os;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws a mesh, placed by the entity's TransformComponent. Opaque and unlit
|
|
||||||
* for now.
|
|
||||||
*/
|
|
||||||
struct MeshComponent {
|
struct MeshComponent {
|
||||||
MeshHandle handle;
|
MeshHandle handle;
|
||||||
// Multiplied with the mesh's vertex colors. Alpha is ignored. Stands in for
|
|
||||||
// a material until there are materials.
|
|
||||||
glm::vec4 color{1.f, 1.f, 1.f, 1.f};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) at its bottom-left to (1, 1) at its
|
|
||||||
// top-right, 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) with
|
|
||||||
// (0, 0) at the texture's top-left.
|
|
||||||
glm::vec4 uvRect{0.f, 0.f, 1.f, 1.f};
|
|
||||||
|
|
||||||
// Higher draws on top, whatever the depth. Sprites with the same sortOrder
|
|
||||||
// draw back to front from the camera; the transform's position.z is depth
|
|
||||||
// like any other axis, not a layer.
|
|
||||||
s32 sortOrder = 0;
|
|
||||||
|
|
||||||
// Maps the unit quad (0, 0)-(1, 1) to world space: to the sprite's
|
|
||||||
// rectangle around its pivot, then by `transform`.
|
|
||||||
glm::mat4 getModel(const TransformComponent& transform) const {
|
|
||||||
return transform.mat4() *
|
|
||||||
glm::translate(glm::mat4{1.f}, glm::vec3{-pivot * size, 0.f}) *
|
|
||||||
glm::scale(glm::mat4{1.f}, glm::vec3{size, 1.f});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CameraComponent {
|
|
||||||
struct Orthographic {
|
|
||||||
float height = 10.f;
|
|
||||||
float nearPlane = -1000.f;
|
|
||||||
float farPlane = 1000.f;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Perspective {
|
|
||||||
float fov = glm::radians(60.f);
|
|
||||||
float nearPlane = 0.1f;
|
|
||||||
float farPlane = 100.f;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class Projection { Orthographic, Perspective };
|
|
||||||
|
|
||||||
Projection projection = Projection::Orthographic;
|
|
||||||
Orthographic ortho;
|
|
||||||
Perspective perspective;
|
|
||||||
|
|
||||||
// View -> clip space. World +Y is up on screen: Vulkan's clip space has +Y
|
|
||||||
// pointing down, so the projection flips it.
|
|
||||||
glm::mat4 getProjection(float aspect) const {
|
|
||||||
glm::mat4 result;
|
|
||||||
if (projection == Projection::Perspective) {
|
|
||||||
result = glm::perspective(perspective.fov, aspect, perspective.nearPlane,
|
|
||||||
perspective.farPlane);
|
|
||||||
} else {
|
|
||||||
float halfHeight = ortho.height / 2.f;
|
|
||||||
float halfWidth = halfHeight * aspect;
|
|
||||||
result = glm::ortho(-halfWidth, halfWidth, -halfHeight, halfHeight,
|
|
||||||
ortho.nearPlane, ortho.farPlane);
|
|
||||||
}
|
|
||||||
|
|
||||||
result[1][1] *= -1.f;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// World -> view space for this camera placed by `transform`. Ignores the
|
|
||||||
// transform's scale; zoom is the projection's job. The camera looks down
|
|
||||||
// view-space -Z.
|
|
||||||
static glm::mat4 getView(const TransformComponent& transform) {
|
|
||||||
glm::mat4 world = glm::translate(glm::mat4{1.f}, transform.position) *
|
|
||||||
glm::mat4_cast(transform.rotation);
|
|
||||||
return glm::inverse(world);
|
|
||||||
}
|
|
||||||
|
|
||||||
// World -> clip space for this camera placed by `transform`.
|
|
||||||
glm::mat4 getViewProjection(const TransformComponent& transform,
|
|
||||||
float aspect) const {
|
|
||||||
return getProjection(aspect) * getView(transform);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <entt/entt.hpp>
|
|
||||||
#include <sophia/scene.hpp>
|
|
||||||
#include <sophia/types.hpp>
|
|
||||||
#include <sophia/util/assert.hpp>
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
class Entity {
|
|
||||||
public:
|
|
||||||
Entity(const Entity&) = default;
|
|
||||||
Entity& operator=(const Entity&) = default;
|
|
||||||
|
|
||||||
Entity() = default;
|
|
||||||
Entity(entt::entity handle, Scene* scene);
|
|
||||||
|
|
||||||
template <typename T, typename... Args>
|
|
||||||
T& addComponent(Args&&... args) {
|
|
||||||
SOPHIA_ASSERT(!hasComponent<T>(), "Entity already has component!");
|
|
||||||
T& component =
|
|
||||||
scene->registry.emplace<T>(handle, std::forward<Args>(args)...);
|
|
||||||
// scene->OnComponentAdded<T>(*this, component);
|
|
||||||
return component;
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
T& getComponent() {
|
|
||||||
SOPHIA_ASSERT(hasComponent<T>(), "Entity does not have component!");
|
|
||||||
return scene->registry.get<T>(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
bool hasComponent() {
|
|
||||||
return scene->registry.all_of<T>(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
void removeComponent() {
|
|
||||||
SOPHIA_ASSERT(hasComponent<T>(), "Entity does not have component!");
|
|
||||||
scene->registry.remove<T>(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
operator bool() const { return handle != entt::null; }
|
|
||||||
operator entt::entity() const { return handle; }
|
|
||||||
operator u32() const { return (u32)handle; }
|
|
||||||
|
|
||||||
// UUID GetUUID() { return GetComponent<IDComponent>().ID; }
|
|
||||||
// const std::string& GetName() { return GetComponent<TagComponent>().Tag; }
|
|
||||||
|
|
||||||
bool operator==(const Entity& other) const {
|
|
||||||
return handle == other.handle && scene == other.scene;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool operator!=(const Entity& other) const { return !(*this == other); }
|
|
||||||
|
|
||||||
private:
|
|
||||||
entt::entity handle{entt::null};
|
|
||||||
Scene* scene = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
template <typename Fn>
|
|
||||||
void Scene::forEachEntity(Fn&& fn) {
|
|
||||||
for (auto [handle] : registry.storage<entt::entity>().each()) {
|
|
||||||
fn(Entity{handle, this});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
#include <any>
|
#include <any>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
#include <sophia/types.hpp>
|
#include <sophia/types.hpp>
|
||||||
#include <sophia/util/delegate.hpp>
|
#include <sophia/util/delegate.hpp>
|
||||||
#include <sophia/util/logger.hpp>
|
#include <sophia/util/logger.hpp>
|
||||||
#include <string>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
#define BIT(x) (1 << (x))
|
#define BIT(x) (1 << (x))
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ class EventSystem {
|
|||||||
this->eventDelegates[eventType] =
|
this->eventDelegates[eventType] =
|
||||||
std::make_unique<std::any>(EventDelegate<E>());
|
std::make_unique<std::any>(EventDelegate<E>());
|
||||||
|
|
||||||
log::trace("EventSystem creating new event type: {}", eventType);
|
log::trace("EventSystem creating new event type: ", eventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
EventDelegate<E>& delegate =
|
EventDelegate<E>& delegate =
|
||||||
@@ -102,7 +103,7 @@ class EventSystem {
|
|||||||
this->eventDelegates[eventType] =
|
this->eventDelegates[eventType] =
|
||||||
std::make_unique<std::any>(EventDelegate<E>());
|
std::make_unique<std::any>(EventDelegate<E>());
|
||||||
|
|
||||||
log::trace("EventSystem creating new event type: {}", eventType);
|
log::trace("EventSystem creating new event type: ", eventType);
|
||||||
}
|
}
|
||||||
|
|
||||||
EventDelegate<E>& delegate =
|
EventDelegate<E>& delegate =
|
||||||
@@ -115,23 +116,19 @@ class EventSystem {
|
|||||||
static_assert(std::is_base_of<Event, E>::value, "E must derive from Event");
|
static_assert(std::is_base_of<Event, E>::value, "E must derive from Event");
|
||||||
EventType eventType = GET_EVENT_TYPE(E);
|
EventType eventType = GET_EVENT_TYPE(E);
|
||||||
|
|
||||||
if (!this->eventDelegates.count(eventType)) {
|
if (!this->eventDelegates.count(eventType)) { return; }
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
EventDelegate<E>& delegate =
|
EventDelegate<E>& delegate =
|
||||||
std::any_cast<EventDelegate<E>&>(*this->eventDelegates.at(eventType));
|
std::any_cast<EventDelegate<E>&>(*this->eventDelegates.at(eventType));
|
||||||
delegate.invoke(event);
|
delegate.invoke(event);
|
||||||
} catch (const std::out_of_range& error) {
|
} catch (const std::out_of_range& error) {
|
||||||
log::error("event dispatch failed: {}", event.toString());
|
log::error("event dispatch failed: ", event.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
EventSystem() {
|
EventSystem() { log::info("EventSystem Initialized: ", this); }
|
||||||
log::info("EventSystem Initialized: {}", static_cast<const void*>(this));
|
|
||||||
}
|
|
||||||
~EventSystem() { log::info("EventSystem Destroyed"); }
|
~EventSystem() { log::info("EventSystem Destroyed"); }
|
||||||
|
|
||||||
std::unordered_map<EventType, std::unique_ptr<std::any>> eventDelegates;
|
std::unordered_map<EventType, std::unique_ptr<std::any>> eventDelegates;
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <sophia/math.hpp>
|
#include <sophia/camera_2d.hpp>
|
||||||
#include <sophia/types.hpp>
|
#include <sophia/types.hpp>
|
||||||
|
|
||||||
|
#define GLM_FORCE_RADIANS
|
||||||
|
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
struct FrameInfo {
|
struct FrameInfo {
|
||||||
@@ -10,6 +14,7 @@ struct FrameInfo {
|
|||||||
double elapsedTime;
|
double elapsedTime;
|
||||||
double deltaTime;
|
double deltaTime;
|
||||||
glm::vec2 currentFramebufferExtent;
|
glm::vec2 currentFramebufferExtent;
|
||||||
|
const Camera2D& camera;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
template <typename T>
|
template <typename Tag>
|
||||||
struct Handle {
|
struct Handle {
|
||||||
static constexpr u32 INVALID_INDEX = (1u << 20) - 1;
|
static constexpr u32 INVALID_INDEX = (1u << 20) - 1;
|
||||||
|
|
||||||
@@ -24,27 +24,10 @@ struct Handle {
|
|||||||
[[nodiscard]] auto operator<=>(const Handle&) const = default;
|
[[nodiscard]] auto operator<=>(const Handle&) const = default;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct InvalidHandle {
|
struct MeshTag {};
|
||||||
template <typename T>
|
struct PanelTag {};
|
||||||
[[nodiscard]] constexpr operator Handle<T>() const {
|
|
||||||
return Handle<T>::invalid();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
inline constexpr InvalidHandle INVALID_HANDLE{};
|
using MeshHandle = Handle<MeshTag>;
|
||||||
|
using PanelHandle = Handle<PanelTag>;
|
||||||
class Mesh;
|
|
||||||
class Panel;
|
|
||||||
class RenderTarget;
|
|
||||||
class Scene;
|
|
||||||
struct SceneView;
|
|
||||||
class Texture;
|
|
||||||
|
|
||||||
using MeshHandle = Handle<Mesh>;
|
|
||||||
using PanelHandle = Handle<Panel>;
|
|
||||||
using RenderTargetHandle = Handle<RenderTarget>;
|
|
||||||
using SceneHandle = Handle<Scene>;
|
|
||||||
using SceneViewHandle = Handle<SceneView>;
|
|
||||||
using TextureHandle = Handle<Texture>;
|
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
|
||||||
#include <glm/gtc/quaternion.hpp>
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
using vec2 = glm::vec2;
|
|
||||||
using vec3 = glm::vec3;
|
|
||||||
using vec4 = glm::vec4;
|
|
||||||
using ivec2 = glm::ivec2;
|
|
||||||
using uvec2 = glm::uvec2;
|
|
||||||
using mat3 = glm::mat3;
|
|
||||||
using mat4 = glm::mat4;
|
|
||||||
using quat = glm::quat;
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <sophia/math.hpp>
|
|
||||||
#include <sophia/types.hpp>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
// Layout must match the per-vertex inputs in mesh.vert.
|
|
||||||
struct MeshVertex {
|
|
||||||
vec3 position{0.f, 0.f, 0.f};
|
|
||||||
vec3 normal{0.f, 0.f, 1.f};
|
|
||||||
// Texture coordinates, with (0, 0) at the texture's top-left.
|
|
||||||
vec2 uv{0.f, 0.f};
|
|
||||||
vec4 color{1.f, 1.f, 1.f, 1.f};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Geometry on the CPU, uploaded with `resources().create<Mesh>(data)`.
|
|
||||||
* Triangles wind counter-clockwise when seen from the front; back faces are
|
|
||||||
* culled. See primitives.hpp for ready-made shapes.
|
|
||||||
*/
|
|
||||||
struct MeshData {
|
|
||||||
std::vector<MeshVertex> vertices;
|
|
||||||
// Empty draws the vertices in order, three per triangle.
|
|
||||||
std::vector<u32> indices;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <sophia/math.hpp>
|
|
||||||
#include <sophia/mesh_data.hpp>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ready-made shapes, centered on the origin. They return MeshData rather than
|
|
||||||
* a mesh so they can be adjusted before uploading:
|
|
||||||
*
|
|
||||||
* MeshHandle cube = resources().create<Mesh>(primitives::cube());
|
|
||||||
*/
|
|
||||||
namespace sophia::primitives {
|
|
||||||
|
|
||||||
// An axis-aligned cube `size` across. Each face has its own four vertices so
|
|
||||||
// it gets its own normal and UVs.
|
|
||||||
MeshData cube(float size = 1.f, vec4 color = vec4{1.f});
|
|
||||||
|
|
||||||
} // namespace sophia::primitives
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <sophia/aabb.hpp>
|
|
||||||
#include <sophia/handles.hpp>
|
|
||||||
#include <sophia/math.hpp>
|
|
||||||
#include <sophia/mesh_data.hpp>
|
|
||||||
#include <sophia/types.hpp>
|
|
||||||
#include <string>
|
|
||||||
#include <type_traits>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
class ResourceRegistry;
|
|
||||||
|
|
||||||
class ResourceRegistryRef {
|
|
||||||
public:
|
|
||||||
template <typename T>
|
|
||||||
Handle<T> load(const std::string& path);
|
|
||||||
|
|
||||||
template <typename T, typename... Args>
|
|
||||||
Handle<T> create(Args&&... args) {
|
|
||||||
return createResource(std::type_identity<T>{}, std::forward<Args>(args)...);
|
|
||||||
}
|
|
||||||
|
|
||||||
void requestResize(RenderTargetHandle handle, u32 width, u32 height);
|
|
||||||
|
|
||||||
glm::vec2 getExtent(RenderTargetHandle handle) const;
|
|
||||||
glm::vec2 getExtent(TextureHandle handle) const;
|
|
||||||
|
|
||||||
// In the mesh's own space.
|
|
||||||
AABB getBounds(MeshHandle handle) const;
|
|
||||||
|
|
||||||
bool isAlive(MeshHandle handle) const;
|
|
||||||
bool isAlive(RenderTargetHandle handle) const;
|
|
||||||
bool isAlive(TextureHandle handle) const;
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
void release(Handle<T> handle);
|
|
||||||
|
|
||||||
private:
|
|
||||||
friend class Application;
|
|
||||||
|
|
||||||
explicit ResourceRegistryRef(ResourceRegistry& registry)
|
|
||||||
: registry{®istry} {}
|
|
||||||
|
|
||||||
MeshHandle createResource(std::type_identity<Mesh>, const MeshData& data);
|
|
||||||
|
|
||||||
RenderTargetHandle createResource(std::type_identity<RenderTarget>,
|
|
||||||
u32 width = 1, u32 height = 1);
|
|
||||||
|
|
||||||
TextureHandle createResource(std::type_identity<Texture>, u32 width,
|
|
||||||
u32 height, std::vector<u8> pixels);
|
|
||||||
|
|
||||||
ResourceRegistry* registry;
|
|
||||||
};
|
|
||||||
|
|
||||||
template <>
|
|
||||||
MeshHandle ResourceRegistryRef::load<Mesh>(const std::string& path);
|
|
||||||
|
|
||||||
template <>
|
|
||||||
TextureHandle ResourceRegistryRef::load<Texture>(const std::string& path);
|
|
||||||
|
|
||||||
template <>
|
|
||||||
void ResourceRegistryRef::release<Mesh>(MeshHandle handle);
|
|
||||||
|
|
||||||
template <>
|
|
||||||
void ResourceRegistryRef::release<RenderTarget>(RenderTargetHandle handle);
|
|
||||||
|
|
||||||
template <>
|
|
||||||
void ResourceRegistryRef::release<Texture>(TextureHandle handle);
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <entt/entt.hpp>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
class Entity;
|
|
||||||
class SceneRenderer; // !TEMP remove this
|
|
||||||
|
|
||||||
class Scene {
|
|
||||||
public:
|
|
||||||
Scene();
|
|
||||||
~Scene();
|
|
||||||
|
|
||||||
Entity createEntity(const std::string& name = {});
|
|
||||||
void destroyEntity(Entity entity);
|
|
||||||
|
|
||||||
Entity createCamera(const std::string& name = "Camera");
|
|
||||||
|
|
||||||
/** Calls `fn(Entity)` for every live entity. Defined in entity.hpp. */
|
|
||||||
template <typename Fn>
|
|
||||||
void forEachEntity(Fn&& fn);
|
|
||||||
|
|
||||||
Entity getActiveCamera();
|
|
||||||
void setActiveCamera(Entity camera);
|
|
||||||
|
|
||||||
bool isVisible() const { return this->visible; }
|
|
||||||
void setVisible(bool visible) { this->visible = visible; }
|
|
||||||
|
|
||||||
bool isActive() const { return this->active; }
|
|
||||||
void setActive(bool active) { this->active = active; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
friend class Entity;
|
|
||||||
friend class SceneRenderer; // !TEMP remove this
|
|
||||||
|
|
||||||
entt::registry registry;
|
|
||||||
entt::entity activeCamera = entt::null;
|
|
||||||
bool visible = true;
|
|
||||||
bool active = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <sophia/handles.hpp>
|
|
||||||
#include <sophia/scene.hpp>
|
|
||||||
#include <sophia/util/pool.hpp>
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
class SceneRegistry {
|
|
||||||
public:
|
|
||||||
SceneRegistry(const SceneRegistry&) = delete;
|
|
||||||
SceneRegistry& operator=(const SceneRegistry&) = delete;
|
|
||||||
|
|
||||||
SceneRegistry() = default;
|
|
||||||
~SceneRegistry() = default;
|
|
||||||
|
|
||||||
SceneHandle create() { return pool.create(); }
|
|
||||||
|
|
||||||
const Scene& get(SceneHandle handle) const { return pool.get(handle); }
|
|
||||||
Scene& get(SceneHandle handle) { return pool.get(handle); }
|
|
||||||
|
|
||||||
const Scene& peek(SceneHandle handle) const { return pool.get(handle); }
|
|
||||||
|
|
||||||
void release(SceneHandle handle) { pool.release(handle); }
|
|
||||||
bool isAlive(SceneHandle handle) const { return pool.isAlive(handle); }
|
|
||||||
|
|
||||||
u32 size() const { return pool.size(); }
|
|
||||||
|
|
||||||
private:
|
|
||||||
Pool<Scene> pool{"Scene"};
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <sophia/entity.hpp>
|
|
||||||
#include <sophia/handles.hpp>
|
|
||||||
#include <sophia/math.hpp>
|
|
||||||
#include <sophia/scene.hpp>
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
enum class ClearFlags : u8 {
|
|
||||||
None = 0,
|
|
||||||
Color = 1 << 0,
|
|
||||||
Depth = 1 << 1,
|
|
||||||
Stencil = 1 << 2,
|
|
||||||
All = Color | Depth | Stencil
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SceneView {
|
|
||||||
SceneHandle scene = INVALID_HANDLE;
|
|
||||||
RenderTargetHandle target =
|
|
||||||
INVALID_HANDLE; // null implies render to swapchain
|
|
||||||
// Null implies the scene's active camera. May belong to another scene,
|
|
||||||
// e.g. an editor camera that isn't part of the scene being shown.
|
|
||||||
Entity camera;
|
|
||||||
|
|
||||||
// TODO(#76): not used yet; every view renders to its whole target.
|
|
||||||
vec4 viewport{0.0f, 0.0f, 1.0f, 1.0f};
|
|
||||||
|
|
||||||
vec4 clearColor{0.f, 0.f, 0.f, 1.f};
|
|
||||||
ClearFlags clearFlags = ClearFlags::All;
|
|
||||||
|
|
||||||
u32 priority = 0; // Lower priority renders first
|
|
||||||
u32 layerMask = 0xFFFFFFFF;
|
|
||||||
|
|
||||||
bool enabled = true; // Disabled views are kept but not rendered
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <sophia/handles.hpp>
|
|
||||||
#include <sophia/scene_view.hpp>
|
|
||||||
#include <sophia/util/pool.hpp>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Every live SceneView. The application renders all enabled views each frame,
|
|
||||||
* in ascending priority order, before the UI.
|
|
||||||
*/
|
|
||||||
class SceneViewRegistry {
|
|
||||||
public:
|
|
||||||
SceneViewRegistry(const SceneViewRegistry&) = delete;
|
|
||||||
SceneViewRegistry& operator=(const SceneViewRegistry&) = delete;
|
|
||||||
|
|
||||||
SceneViewRegistry() = default;
|
|
||||||
~SceneViewRegistry() = default;
|
|
||||||
|
|
||||||
SceneViewHandle create(const SceneView& view = {}) {
|
|
||||||
isRenderQueueDirty = true;
|
|
||||||
return pool.create(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
const SceneView& get(SceneViewHandle handle) const {
|
|
||||||
return pool.get(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
SceneView& get(SceneViewHandle handle) {
|
|
||||||
isRenderQueueDirty = true;
|
|
||||||
return pool.get(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
const SceneView& peek(SceneViewHandle handle) const {
|
|
||||||
return pool.get(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
void release(SceneViewHandle handle) {
|
|
||||||
isRenderQueueDirty = true;
|
|
||||||
pool.release(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isAlive(SceneViewHandle handle) const { return pool.isAlive(handle); }
|
|
||||||
|
|
||||||
u32 size() const { return pool.size(); }
|
|
||||||
|
|
||||||
template <typename Fn>
|
|
||||||
void forEach(Fn&& fn) {
|
|
||||||
isRenderQueueDirty = true;
|
|
||||||
pool.forEach(std::forward<Fn>(fn));
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename Fn>
|
|
||||||
void forEach(Fn&& fn) const {
|
|
||||||
pool.forEach(std::forward<Fn>(fn));
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::vector<SceneViewHandle>& GetRenderQueue() const {
|
|
||||||
if (isRenderQueueDirty) {
|
|
||||||
rebuildRenderQueue();
|
|
||||||
}
|
|
||||||
return renderQueue;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
void rebuildRenderQueue() const {
|
|
||||||
renderQueue.clear();
|
|
||||||
pool.forEachWithHandle([&](SceneViewHandle handle, const SceneView& view) {
|
|
||||||
if (view.enabled) {
|
|
||||||
renderQueue.push_back(handle);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
std::ranges::stable_sort(renderQueue, {}, [this](SceneViewHandle handle) {
|
|
||||||
return pool.get(handle).priority;
|
|
||||||
});
|
|
||||||
isRenderQueueDirty = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Pool<SceneView> pool{"SceneView"};
|
|
||||||
|
|
||||||
mutable std::vector<SceneViewHandle> renderQueue;
|
|
||||||
mutable bool isRenderQueueDirty = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -17,9 +17,9 @@ class DebugUI {
|
|||||||
void show();
|
void show();
|
||||||
void hide();
|
void hide();
|
||||||
|
|
||||||
void showPanelRegistry(bool show = false);
|
void showPanelRegistry(bool show = true);
|
||||||
void showFrameStats(bool show = false);
|
void showFrameStats(bool show = true);
|
||||||
void showConsole(bool show = false);
|
void showConsole(bool show = true);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
PanelRegistry& registry;
|
PanelRegistry& registry;
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
struct DockLayout {
|
|
||||||
float leftWidth = 0.2f;
|
|
||||||
float rightWidth = 0.25f;
|
|
||||||
float bottomHeight = 0.25f;
|
|
||||||
|
|
||||||
std::vector<std::string> left;
|
|
||||||
std::vector<std::string> right;
|
|
||||||
std::vector<std::string> bottom;
|
|
||||||
std::vector<std::string> center;
|
|
||||||
|
|
||||||
bool empty() const {
|
|
||||||
return left.empty() && right.empty() && bottom.empty() && center.empty();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <imgui.h>
|
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
|
||||||
#include <optional>
|
|
||||||
#include <sophia/frame_info.hpp>
|
#include <sophia/frame_info.hpp>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -12,54 +8,28 @@ namespace sophia {
|
|||||||
|
|
||||||
class Panel {
|
class Panel {
|
||||||
public:
|
public:
|
||||||
struct Config {
|
|
||||||
const std::string name;
|
|
||||||
|
|
||||||
ImGuiWindowFlags windowFlags = ImGuiWindowFlags_None;
|
|
||||||
ImGuiDockNodeFlags dockNodeFlags = ImGuiDockNodeFlags_None;
|
|
||||||
|
|
||||||
bool locked = false;
|
|
||||||
bool closable = true;
|
|
||||||
|
|
||||||
glm::vec2 size{0.0f, 0.0f};
|
|
||||||
std::optional<glm::vec2> position;
|
|
||||||
|
|
||||||
ImGuiWindowFlags getWindowFlags() const;
|
|
||||||
ImGuiDockNodeFlags getDockNodeFlags() const;
|
|
||||||
bool hasCloseButton() const;
|
|
||||||
ImGuiCond getPlacementCond() const;
|
|
||||||
};
|
|
||||||
|
|
||||||
Panel(const Panel&) = delete;
|
Panel(const Panel&) = delete;
|
||||||
Panel& operator=(const Panel&) = delete;
|
Panel& operator=(const Panel&) = delete;
|
||||||
|
|
||||||
Panel(Panel&&) noexcept = default;
|
Panel(Panel&&) noexcept = default;
|
||||||
Panel& operator=(Panel&&) = delete;
|
Panel& operator=(Panel&&) noexcept = default;
|
||||||
|
|
||||||
explicit Panel(Config config) : config{std::move(config)} {}
|
explicit Panel(std::string name = {}) : name{std::move(name)} {}
|
||||||
virtual ~Panel() = default;
|
virtual ~Panel() = default;
|
||||||
|
|
||||||
void draw(const FrameInfo& frameInfo);
|
virtual void onUpdate(const FrameInfo& frameInfo) = 0;
|
||||||
|
|
||||||
Config& getConfig() { return config; }
|
const std::string& getName() const { return name; }
|
||||||
const Config& getConfig() const { return config; }
|
|
||||||
|
|
||||||
const std::string& getName() const { return config.name; }
|
|
||||||
bool isLocked() const { return config.locked; }
|
|
||||||
|
|
||||||
bool isVisible() const { return this->visible; }
|
bool isVisible() const { return this->visible; }
|
||||||
bool isShown() const { return this->visible && this->shown; }
|
|
||||||
void setVisible(bool visible) { this->visible = visible; }
|
void setVisible(bool visible) { this->visible = visible; }
|
||||||
void toggleVisible() { this->visible = !this->visible; }
|
void toggleVisible() { this->visible = !this->visible; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void onUpdate(const FrameInfo& frameInfo) = 0;
|
|
||||||
|
|
||||||
bool visible = true;
|
bool visible = true;
|
||||||
bool shown = false;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Config config;
|
std::string name;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
|
|||||||
@@ -3,10 +3,8 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <sophia/frame_info.hpp>
|
#include <sophia/frame_info.hpp>
|
||||||
#include <sophia/handles.hpp>
|
#include <sophia/handles.hpp>
|
||||||
#include <sophia/ui/dock_layout.hpp>
|
|
||||||
#include <sophia/ui/panel.hpp>
|
#include <sophia/ui/panel.hpp>
|
||||||
#include <sophia/util/assert.hpp>
|
#include <sophia/util/assert.hpp>
|
||||||
#include <sophia/util/pool.hpp>
|
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <typeinfo>
|
#include <typeinfo>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -22,23 +20,23 @@ class PanelRegistry {
|
|||||||
PanelRegistry() = default;
|
PanelRegistry() = default;
|
||||||
~PanelRegistry() = default;
|
~PanelRegistry() = default;
|
||||||
|
|
||||||
PanelHandle add(std::unique_ptr<Panel> panel) {
|
PanelHandle add(std::unique_ptr<Panel> panel);
|
||||||
return pool.add(std::move(panel));
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T, typename... Args>
|
template <typename T, typename... Args>
|
||||||
PanelHandle emplace(Args&&... args) {
|
PanelHandle emplace(Args&&... args) {
|
||||||
return add(std::make_unique<T>(std::forward<Args>(args)...));
|
return add(std::make_unique<T>(std::forward<Args>(args)...));
|
||||||
}
|
}
|
||||||
|
|
||||||
const Panel& get(PanelHandle handle) const { return pool.get(handle); }
|
const Panel& get(PanelHandle handle) const;
|
||||||
Panel& get(PanelHandle handle) { return pool.get(handle); }
|
Panel& get(PanelHandle handle) {
|
||||||
|
return const_cast<Panel&>(std::as_const(*this).get(handle));
|
||||||
|
}
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
const T& get(PanelHandle handle) const {
|
const T& get(PanelHandle handle) const {
|
||||||
const Panel& panel = get(handle);
|
const Panel& panel = get(handle);
|
||||||
SOPHIA_ASSERT(dynamic_cast<const T*>(&panel) != nullptr,
|
SOPHIA_ASSERT(dynamic_cast<const T*>(&panel) != nullptr,
|
||||||
"PanelHandle does not refer to a {}", typeid(T).name());
|
"PanelHandle does not refer to a", typeid(T).name());
|
||||||
return static_cast<const T&>(panel);
|
return static_cast<const T&>(panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,17 +45,19 @@ class PanelRegistry {
|
|||||||
return const_cast<T&>(std::as_const(*this).template get<T>(handle));
|
return const_cast<T&>(std::as_const(*this).template get<T>(handle));
|
||||||
}
|
}
|
||||||
|
|
||||||
void remove(PanelHandle handle) { pool.release(handle); }
|
void remove(PanelHandle handle);
|
||||||
bool isValid(PanelHandle handle) const { return pool.isAlive(handle); }
|
bool isValid(PanelHandle handle) const;
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
const T* find() const {
|
const T* find() const {
|
||||||
const T* result = nullptr;
|
for (const Slot& slot : slots) {
|
||||||
pool.findIf([&result](const Panel& panel) {
|
if (slot.panel) {
|
||||||
result = dynamic_cast<const T*>(&panel);
|
if (const T* panel = dynamic_cast<const T*>(slot.panel.get())) {
|
||||||
return result != nullptr;
|
return panel;
|
||||||
});
|
}
|
||||||
return result;
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
@@ -66,35 +66,42 @@ class PanelRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Panel* find(std::string_view name) const {
|
const Panel* find(std::string_view name) const {
|
||||||
return pool.findIf(
|
for (const Slot& slot : slots) {
|
||||||
[name](const Panel& panel) { return panel.getName() == name; });
|
if (slot.panel && slot.panel->getName() == name) {
|
||||||
|
return slot.panel.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
Panel* find(std::string_view name) {
|
Panel* find(std::string_view name) {
|
||||||
return const_cast<Panel*>(std::as_const(*this).find(name));
|
return const_cast<Panel*>(std::as_const(*this).find(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 size() const { return pool.size(); }
|
u32 size() const;
|
||||||
u32 visibleCount() const;
|
u32 visibleCount() const;
|
||||||
|
|
||||||
std::vector<std::string_view> names() const {
|
std::vector<std::string_view> names() const {
|
||||||
std::vector<std::string_view> result;
|
std::vector<std::string_view> result;
|
||||||
result.reserve(pool.size());
|
result.reserve(slots.size());
|
||||||
pool.forEach(
|
for (const Slot& slot : slots) {
|
||||||
[&result](const Panel& panel) { result.push_back(panel.getName()); });
|
if (slot.panel) {
|
||||||
|
result.push_back(slot.panel->getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void update(const FrameInfo& frameInfo);
|
void update(const FrameInfo& frameInfo);
|
||||||
|
|
||||||
void setDefaultLayout(DockLayout layout) {
|
|
||||||
defaultLayout = std::move(layout);
|
|
||||||
}
|
|
||||||
const DockLayout& getDefaultLayout() const { return defaultLayout; }
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Pool<Panel> pool{"Panel"};
|
struct Slot {
|
||||||
DockLayout defaultLayout;
|
std::unique_ptr<Panel> panel;
|
||||||
|
u32 generation = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<Slot> slots;
|
||||||
|
std::vector<u32> freeList;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <imgui.h>
|
|
||||||
|
|
||||||
#include <sophia/handles.hpp>
|
|
||||||
#include <sophia/resource_registry_ref.hpp>
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
class ImGuiRenderer;
|
|
||||||
|
|
||||||
class UI {
|
|
||||||
public:
|
|
||||||
UI(const UI&) = delete;
|
|
||||||
UI& operator=(const UI&) = delete;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws the target filling the space left in the current window, and keeps
|
|
||||||
* the target sized to match. The target catches up on the next frame, so the
|
|
||||||
* image may be a frame behind while the window is being resized.
|
|
||||||
*/
|
|
||||||
void image(RenderTargetHandle handle);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Texture to hand to ImGui::Image() that shows the target's contents.
|
|
||||||
* Changes when the target is resized, so fetch it each frame rather than
|
|
||||||
* caching it.
|
|
||||||
*/
|
|
||||||
ImTextureID getTextureId(RenderTargetHandle handle);
|
|
||||||
|
|
||||||
private:
|
|
||||||
friend class Application;
|
|
||||||
|
|
||||||
explicit UI(ResourceRegistryRef resources) : resources{resources} {}
|
|
||||||
|
|
||||||
ResourceRegistryRef resources;
|
|
||||||
ImGuiRenderer* renderer = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Usage:
|
* Usage:
|
||||||
* SOPHIA_ASSERT(pipelineLayout != nullptr);
|
* SOPHIA_ASSERT(pipelineLayout != nullptr);
|
||||||
* SOPHIA_ASSERT(index < count, "index {} out of bounds: {}", index, count);
|
* SOPHIA_ASSERT(index < count, "index", index, "out of bounds:", count);
|
||||||
*
|
|
||||||
* The optional message is a std::format string, checked at compile time.
|
|
||||||
*
|
*
|
||||||
* Compiled out entirely in release (NDEBUG) builds, matching <cassert>'s
|
* Compiled out entirely in release (NDEBUG) builds, matching <cassert>'s
|
||||||
* assert() semantics: don't rely on side effects inside the condition.
|
* assert() semantics: don't rely on side effects inside the condition.
|
||||||
@@ -12,16 +10,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <format>
|
|
||||||
#include <sophia/util/logger.hpp>
|
#include <sophia/util/logger.hpp>
|
||||||
|
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
#define SOPHIA_ASSERT(condition, ...) \
|
#define SOPHIA_ASSERT(condition, ...) \
|
||||||
do { \
|
do { \
|
||||||
if (!(condition)) { \
|
if (!(condition)) { \
|
||||||
::sophia::log::fatal( \
|
::sophia::log::fatal("Assertion failed:", #condition, "at", __FILE__, \
|
||||||
"Assertion failed: {} at {}:{}" __VA_OPT__(" - {}"), #condition, \
|
":", __LINE__ __VA_OPT__(, ) __VA_ARGS__); \
|
||||||
__FILE__, __LINE__ __VA_OPT__(, ::std::format(__VA_ARGS__))); \
|
|
||||||
std::abort(); \
|
std::abort(); \
|
||||||
} \
|
} \
|
||||||
} while (false)
|
} while (false)
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <sophia/util/logger.hpp>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <typeinfo>
|
#include <typeinfo>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <sophia/util/logger.hpp>
|
||||||
|
|
||||||
#define DEFAULT_DELEGATE_FUNCTOR_ALLOC 5
|
#define DEFAULT_DELEGATE_FUNCTOR_ALLOC 5
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
@@ -38,9 +39,7 @@ class expired_weak_object : public std::runtime_error {
|
|||||||
template <class O, typename... A>
|
template <class O, typename... A>
|
||||||
std::function<void(A...)> createWeakFunction(void (O::*method)(A...),
|
std::function<void(A...)> createWeakFunction(void (O::*method)(A...),
|
||||||
std::shared_ptr<O> obj) {
|
std::shared_ptr<O> obj) {
|
||||||
if (!obj) {
|
if (!obj) { throw std::invalid_argument("method and obj must not be null"); }
|
||||||
throw std::invalid_argument("method and obj must not be null");
|
|
||||||
}
|
|
||||||
|
|
||||||
std::weak_ptr<O> weakObj = obj;
|
std::weak_ptr<O> weakObj = obj;
|
||||||
|
|
||||||
@@ -83,7 +82,7 @@ class Delegate {
|
|||||||
try {
|
try {
|
||||||
this->functors[i](std::forward<A>(args)...);
|
this->functors[i](std::forward<A>(args)...);
|
||||||
} catch (const expired_weak_object& e) {
|
} catch (const expired_weak_object& e) {
|
||||||
log::error("delegate invoke error: {}", e.what());
|
log::error("delegate invoke error: ", e.what());
|
||||||
deadFunctorIndexs.push_back(i);
|
deadFunctorIndexs.push_back(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,9 +97,7 @@ class Delegate {
|
|||||||
void operator=(Functor c) {
|
void operator=(Functor c) {
|
||||||
clear();
|
clear();
|
||||||
|
|
||||||
if (c != nullptr) {
|
if (c != nullptr) { add(c); }
|
||||||
add(c);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void operator+=(Functor c) { add(c); }
|
void operator+=(Functor c) { add(c); }
|
||||||
|
|||||||
@@ -1,35 +1,22 @@
|
|||||||
/**
|
/**
|
||||||
* std::format-style logging. Format strings are checked at compile time.
|
* Lots of work still needs to be done in here.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* using namespace sophia;
|
* using namespace sophia;
|
||||||
* log::fatal("failed to load {}", path)
|
* log::fatal("fatal message")
|
||||||
* log::info("loaded {} meshes in {:.2f}ms", count, ms)
|
* log::info("Infomation message")
|
||||||
* log::debug("value: {}", x) // prefixed with file:line
|
|
||||||
* log::print("no prefix, just the message")
|
|
||||||
*
|
|
||||||
* Everything is compiled out in release (NDEBUG) builds.
|
|
||||||
*
|
*
|
||||||
* TODO Impl some form of log error handling
|
* TODO Impl some form of log error handling
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdio>
|
#include <iostream>
|
||||||
#include <format>
|
|
||||||
#include <print>
|
|
||||||
#include <source_location>
|
|
||||||
#include <string_view>
|
|
||||||
#include <type_traits>
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace sophia::log {
|
#include <sophia/types.hpp>
|
||||||
|
|
||||||
#ifndef NDEBUG
|
namespace sophia::log {
|
||||||
inline constexpr bool enabled = true;
|
|
||||||
#else
|
|
||||||
inline constexpr bool enabled = false;
|
|
||||||
#endif // !NDEBUG
|
|
||||||
|
|
||||||
enum class LEVEL {
|
enum class LEVEL {
|
||||||
NONE,
|
NONE,
|
||||||
@@ -42,113 +29,98 @@ enum class LEVEL {
|
|||||||
TRACE,
|
TRACE,
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace detail {
|
// Helper print functions
|
||||||
|
#define LOG_PRINT_HELPER(MSG) std::cout << MSG
|
||||||
|
#define LOG_PREFIX_FATAL LOG_PRINT_HELPER("\033[1;91m[FATAL]\033[0m ")
|
||||||
|
#define LOG_PREFIX_ERROR LOG_PRINT_HELPER("\033[1;31m[ERROR]\033[0m ")
|
||||||
|
#define LOG_PREFIX_WARNING LOG_PRINT_HELPER("\033[1;33m[WARNING]\033[0m ")
|
||||||
|
#define LOG_PREFIX_INFO LOG_PRINT_HELPER("\033[32m[INFO]\033[0m ")
|
||||||
|
#define LOG_PREFIX_VERBOSE LOG_PRINT_HELPER("\033[36m[VERBOSE]\033[0m ")
|
||||||
|
#define LOG_PREFIX_DEBUG LOG_PRINT_HELPER("\033[1;34m[DEBUG]\033[0m ")
|
||||||
|
#define LOG_PREFIX_TRACE LOG_PRINT_HELPER("\033[1;90m[TRACE]\033[0m ")
|
||||||
|
|
||||||
constexpr std::string_view prefix(LEVEL level) {
|
template <typename... Args>
|
||||||
|
void log(LEVEL level = LEVEL::NONE, Args... args) {
|
||||||
|
#ifndef NDEBUG
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case LEVEL::FATAL:
|
case LEVEL::FATAL:
|
||||||
return "\033[1;91m[FATAL]\033[0m ";
|
LOG_PREFIX_FATAL;
|
||||||
|
break;
|
||||||
case LEVEL::ERROR:
|
case LEVEL::ERROR:
|
||||||
return "\033[1;31m[ERROR]\033[0m ";
|
LOG_PREFIX_ERROR;
|
||||||
|
break;
|
||||||
case LEVEL::WARNING:
|
case LEVEL::WARNING:
|
||||||
return "\033[1;33m[WARNING]\033[0m ";
|
LOG_PREFIX_WARNING;
|
||||||
|
break;
|
||||||
case LEVEL::INFO:
|
case LEVEL::INFO:
|
||||||
return "\033[32m[INFO]\033[0m ";
|
LOG_PREFIX_INFO;
|
||||||
|
break;
|
||||||
case LEVEL::VERBOSE:
|
case LEVEL::VERBOSE:
|
||||||
return "\033[36m[VERBOSE]\033[0m ";
|
LOG_PREFIX_VERBOSE;
|
||||||
case LEVEL::DEBUGGING:
|
break;
|
||||||
return "\033[1;34m[DEBUG]\033[0m ";
|
// case LEVEL::DEBUGGING:
|
||||||
|
// LOG_PREFIX_DEBUG;
|
||||||
|
// break;
|
||||||
case LEVEL::TRACE:
|
case LEVEL::TRACE:
|
||||||
return "\033[1;90m[TRACE]\033[0m ";
|
LOG_PREFIX_TRACE;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return "\033[1;90m[LOG]\033[0m ";
|
std::cout << "\033[1;90m[LOG]\033[0m ";
|
||||||
}
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void write(std::string_view prefix, std::string_view message) {
|
((std::cout << ' ' << std::forward<Args>(args)), ...);
|
||||||
std::println(stdout, "{}{}", prefix, message);
|
std::cout << std::endl;
|
||||||
std::fflush(stdout);
|
#else
|
||||||
}
|
(void)level;
|
||||||
|
(void)std::initializer_list<int>{((void)args, 0)...};
|
||||||
// Captures the caller's location alongside the format string, since a
|
#endif // !NDEBUG
|
||||||
// defaulted std::source_location parameter can't follow a parameter pack.
|
|
||||||
template <typename... Args>
|
|
||||||
struct FormatWithLocation {
|
|
||||||
std::format_string<Args...> fmt;
|
|
||||||
std::source_location location;
|
|
||||||
|
|
||||||
template <typename S>
|
|
||||||
requires std::is_convertible_v<const S&, std::string_view>
|
|
||||||
consteval FormatWithLocation(
|
|
||||||
const S& fmt,
|
|
||||||
std::source_location location = std::source_location::current())
|
|
||||||
: fmt(fmt), location(location) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace detail
|
|
||||||
|
|
||||||
template <typename... Args>
|
|
||||||
void log(LEVEL level, std::format_string<Args...> fmt, Args&&... args) {
|
|
||||||
if constexpr (enabled) {
|
|
||||||
detail::write(detail::prefix(level),
|
|
||||||
std::format(fmt, std::forward<Args>(args)...));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename... Args>
|
template <typename... Args>
|
||||||
void fatal(std::format_string<Args...> fmt, Args&&... args) {
|
void fatal(Args... args) {
|
||||||
log(LEVEL::FATAL, fmt, std::forward<Args>(args)...);
|
log(LEVEL::FATAL, args...);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename... Args>
|
template <typename... Args>
|
||||||
void error(std::format_string<Args...> fmt, Args&&... args) {
|
void error(Args... args) {
|
||||||
log(LEVEL::ERROR, fmt, std::forward<Args>(args)...);
|
log(LEVEL::ERROR, args...);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename... Args>
|
template <typename... Args>
|
||||||
void warning(std::format_string<Args...> fmt, Args&&... args) {
|
void warning(Args... args) {
|
||||||
log(LEVEL::WARNING, fmt, std::forward<Args>(args)...);
|
log(LEVEL::WARNING, args...);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename... Args>
|
template <typename... Args>
|
||||||
void info(std::format_string<Args...> fmt, Args&&... args) {
|
void info(Args... args) {
|
||||||
log(LEVEL::INFO, fmt, std::forward<Args>(args)...);
|
log(LEVEL::INFO, args...);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename... Args>
|
template <typename... Args>
|
||||||
void verbose(std::format_string<Args...> fmt, Args&&... args) {
|
void verbose(Args... args) {
|
||||||
log(LEVEL::VERBOSE, fmt, std::forward<Args>(args)...);
|
log(LEVEL::VERBOSE, args...);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename... Args>
|
template <typename... Args>
|
||||||
void debug(detail::FormatWithLocation<std::type_identity_t<Args>...> fmt,
|
void debug(const char* file, u16 line, Args... args) {
|
||||||
Args&&... args) {
|
#ifndef NDEBUG
|
||||||
if constexpr (enabled) {
|
LOG_PREFIX_DEBUG;
|
||||||
detail::write(
|
std::cout << file << "::" << line;
|
||||||
detail::prefix(LEVEL::DEBUGGING),
|
((std::cout << ' ' << std::forward<Args>(args)), ...);
|
||||||
std::format("{}:{} {}", fmt.location.file_name(), fmt.location.line(),
|
std::cout << std::endl;
|
||||||
std::format(fmt.fmt, std::forward<Args>(args)...)));
|
#else
|
||||||
}
|
(void)file;
|
||||||
|
(void)line;
|
||||||
|
(void)std::initializer_list<int>{((void)args, 0)...};
|
||||||
|
#endif // !NDEBUG
|
||||||
}
|
}
|
||||||
|
// trick
|
||||||
|
#define debug(...) debug(__FILE__, __LINE__, __VA_ARGS__)
|
||||||
|
|
||||||
template <typename... Args>
|
template <typename... Args>
|
||||||
void trace(std::format_string<Args...> fmt, Args&&... args) {
|
void trace(Args... args) {
|
||||||
log(LEVEL::TRACE, fmt, std::forward<Args>(args)...);
|
log(LEVEL::TRACE, args...);
|
||||||
}
|
|
||||||
|
|
||||||
// Like the functions above, but with no "[LEVEL]" prefix. With no arguments,
|
|
||||||
// an empty line.
|
|
||||||
inline void print() {
|
|
||||||
if constexpr (enabled) {
|
|
||||||
detail::write("", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename... Args>
|
|
||||||
void print(std::format_string<Args...> fmt, Args&&... args) {
|
|
||||||
if constexpr (enabled) {
|
|
||||||
detail::write("", std::format(fmt, std::forward<Args>(args)...));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace sophia::log
|
} // namespace sophia::log
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
#include <sophia/handles.hpp>
|
|
||||||
#include <sophia/types.hpp>
|
|
||||||
#include <sophia/util/assert.hpp>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
class Pool {
|
|
||||||
public:
|
|
||||||
explicit Pool(const char* name) : name{name} {}
|
|
||||||
|
|
||||||
Pool(const Pool&) = delete;
|
|
||||||
Pool& operator=(const Pool&) = delete;
|
|
||||||
|
|
||||||
template <typename... Args>
|
|
||||||
Handle<T> create(Args&&... args) {
|
|
||||||
return add(std::make_unique<T>(std::forward<Args>(args)...));
|
|
||||||
}
|
|
||||||
|
|
||||||
Handle<T> add(std::unique_ptr<T> item) {
|
|
||||||
if (!freeList.empty()) {
|
|
||||||
u32 index = freeList.back();
|
|
||||||
freeList.pop_back();
|
|
||||||
slots[index].item = std::move(item);
|
|
||||||
return Handle<T>{index, slots[index].generation};
|
|
||||||
}
|
|
||||||
|
|
||||||
slots.push_back(Slot{std::move(item), 0});
|
|
||||||
return Handle<T>{static_cast<u32>(slots.size() - 1), 0};
|
|
||||||
}
|
|
||||||
|
|
||||||
T& get(Handle<T> handle) {
|
|
||||||
return const_cast<T&>(std::as_const(*this).get(handle));
|
|
||||||
}
|
|
||||||
|
|
||||||
const T& get(Handle<T> handle) const { return *validate(handle).item; }
|
|
||||||
|
|
||||||
void release(Handle<T> handle) {
|
|
||||||
validate(handle);
|
|
||||||
|
|
||||||
Slot& slot = slots[handle.index];
|
|
||||||
slot.item.reset();
|
|
||||||
slot.generation = (slot.generation + 1) & GENERATION_MASK;
|
|
||||||
freeList.push_back(handle.index);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isAlive(Handle<T> handle) const {
|
|
||||||
return handle.isValid() && handle.index < slots.size() &&
|
|
||||||
slots[handle.index].generation == handle.generation &&
|
|
||||||
slots[handle.index].item != nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
u32 size() const { return static_cast<u32>(slots.size() - freeList.size()); }
|
|
||||||
|
|
||||||
template <typename Fn>
|
|
||||||
void forEach(Fn&& fn) {
|
|
||||||
for (Slot& slot : slots) {
|
|
||||||
if (slot.item) {
|
|
||||||
fn(*slot.item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename Fn>
|
|
||||||
void forEach(Fn&& fn) const {
|
|
||||||
for (const Slot& slot : slots) {
|
|
||||||
if (slot.item) {
|
|
||||||
fn(*slot.item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Calls `fn(handle, item)` for every live item. */
|
|
||||||
template <typename Fn>
|
|
||||||
void forEachWithHandle(Fn&& fn) const {
|
|
||||||
for (u32 index = 0; index < slots.size(); index++) {
|
|
||||||
const Slot& slot = slots[index];
|
|
||||||
if (slot.item) {
|
|
||||||
fn(Handle<T>{index, slot.generation}, *slot.item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename Predicate>
|
|
||||||
T* findIf(Predicate&& predicate) {
|
|
||||||
return const_cast<T*>(
|
|
||||||
std::as_const(*this).findIf(std::forward<Predicate>(predicate)));
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename Predicate>
|
|
||||||
const T* findIf(Predicate&& predicate) const {
|
|
||||||
for (const Slot& slot : slots) {
|
|
||||||
if (slot.item && predicate(*slot.item)) {
|
|
||||||
return slot.item.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
static constexpr u32 GENERATION_MASK = (1u << 12) - 1;
|
|
||||||
|
|
||||||
struct Slot {
|
|
||||||
std::unique_ptr<T> item;
|
|
||||||
u32 generation = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Slot& validate(Handle<T> handle) const {
|
|
||||||
SOPHIA_ASSERT(handle.isValid(), "invalid handle: {}", name);
|
|
||||||
SOPHIA_ASSERT(handle.index < slots.size(), "{} handle index out of range: {}", name, static_cast<u32>(handle.index));
|
|
||||||
|
|
||||||
const Slot& slot = slots[handle.index];
|
|
||||||
SOPHIA_ASSERT(slot.generation == handle.generation, "stale {} handle (generation mismatch): {}", name, static_cast<u32>(handle.index));
|
|
||||||
SOPHIA_ASSERT(slot.item != nullptr, "{} handle refers to a released item: {}", name, static_cast<u32>(handle.index));
|
|
||||||
return slot;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* name;
|
|
||||||
std::vector<Slot> slots;
|
|
||||||
std::vector<u32> freeList;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
+79
-203
@@ -1,48 +1,46 @@
|
|||||||
#include <imgui.h>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <glm/glm.hpp>
|
|
||||||
#include <glm/gtc/matrix_transform.hpp>
|
|
||||||
#include <map>
|
|
||||||
#include <optional>
|
|
||||||
#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 "renderer/imgui_renderer.hpp"
|
#define GLM_FORCE_RADIANS
|
||||||
#include "renderer/model_render_system.hpp"
|
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
|
||||||
#include "renderer/render_context.hpp"
|
#include <imgui.h>
|
||||||
#include "renderer/render_graph.hpp"
|
|
||||||
#include "renderer/scene_renderer.hpp"
|
#include <glm/glm.hpp>
|
||||||
#include "resources/mesh.hpp"
|
|
||||||
#include "resources/render_target.hpp"
|
#include "buffer.hpp"
|
||||||
#include "resources/resource_registry.hpp"
|
#include "device.hpp"
|
||||||
#include "vulkan/buffer.hpp"
|
#include "imgui_renderer.hpp"
|
||||||
#include "vulkan/device.hpp"
|
#include "mesh.hpp"
|
||||||
#include "vulkan/pipelines/graphics_pipeline.hpp"
|
#include "pipelines/graphics_pipeline.hpp"
|
||||||
#include "vulkan/shader.hpp"
|
#include "render_context.hpp"
|
||||||
|
#include "shader.hpp"
|
||||||
|
#include "systems/model_render_system.hpp"
|
||||||
#include "window.hpp"
|
#include "window.hpp"
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
glm::vec2 position;
|
||||||
|
glm::vec3 color;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PushConstants {
|
||||||
|
glm::mat4 transform;
|
||||||
|
glm::vec4 color;
|
||||||
|
glm::vec4 data;
|
||||||
|
};
|
||||||
|
|
||||||
std::vector<std::string> commandLineArgs;
|
std::vector<std::string> commandLineArgs;
|
||||||
|
|
||||||
constexpr ClearColor CLEAR_COLOR = {0.01f, 0.01f, 0.02f, 1.0f};
|
|
||||||
|
|
||||||
bool hasFlag(ClearFlags flags, ClearFlags flag) {
|
|
||||||
return (static_cast<u8>(flags) & static_cast<u8>(flag)) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
class Application::Impl {
|
class Application::Impl {
|
||||||
@@ -52,41 +50,15 @@ class Application::Impl {
|
|||||||
config{config},
|
config{config},
|
||||||
window{config.width, config.height, config.name},
|
window{config.width, config.height, config.name},
|
||||||
device{&window},
|
device{&window},
|
||||||
renderContext{window, device},
|
renderContext{window, device} {}
|
||||||
resourceRegistry{device, renderContext.getImageFormat()} {}
|
|
||||||
|
|
||||||
void run() {
|
void run() {
|
||||||
ModelRenderSystem modelRenderSystem{device, renderContext};
|
ModelRenderSystem modelRenderSystem{device, renderContext};
|
||||||
SceneRenderer sceneRenderer{this->application.sceneRegistry,
|
|
||||||
this->resourceRegistry, device,
|
|
||||||
renderContext.getImageFormat()};
|
|
||||||
ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(),
|
ImGuiRenderer imguiRenderer{window, device, renderContext.getImageFormat(),
|
||||||
renderContext.getImageCount(),
|
renderContext.getImageCount()};
|
||||||
this->resourceRegistry};
|
|
||||||
this->application._ui.renderer = &imguiRenderer;
|
|
||||||
|
|
||||||
this->application.consoleCommandRegistry.registerCommand(
|
|
||||||
"reset_layout", "Restore the default panel layout",
|
|
||||||
[&imguiRenderer](const std::vector<std::string>&) {
|
|
||||||
imguiRenderer.resetLayout();
|
|
||||||
return std::string{"Layout reset"};
|
|
||||||
});
|
|
||||||
|
|
||||||
this->application.onCreate();
|
this->application.onCreate();
|
||||||
|
|
||||||
auto& meshPool = this->resourceRegistry.meshPool;
|
|
||||||
auto& renderTargetPool = this->resourceRegistry.renderTargetPool;
|
|
||||||
|
|
||||||
PanelRegistry& panelRegistry = this->application.panelRegistry;
|
|
||||||
SceneRegistry& sceneRegistry = this->application.sceneRegistry;
|
|
||||||
SceneViewRegistry& sceneViewRegistry = this->application.sceneViewRegistry;
|
|
||||||
|
|
||||||
imguiRenderer.setDefaultLayout(panelRegistry.getDefaultLayout());
|
|
||||||
|
|
||||||
// Views already warned about, so each warns once.
|
|
||||||
std::set<SceneViewHandle> viewsWithoutCamera;
|
|
||||||
std::set<SceneViewHandle> viewsOverwritingOthers;
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -102,24 +74,14 @@ class Application::Impl {
|
|||||||
double elapsedTime =
|
double elapsedTime =
|
||||||
std::chrono::duration<double>(currentTime - startTime).count();
|
std::chrono::duration<double>(currentTime - startTime).count();
|
||||||
|
|
||||||
// Resizing waits for the GPU, so it has to happen before any of this
|
|
||||||
// frame's work (or ImGui's references to the targets) is recorded.
|
|
||||||
renderTargetPool.applyPendingResizes();
|
|
||||||
|
|
||||||
vk::Extent2D extent = renderContext.getExtent();
|
vk::Extent2D extent = renderContext.getExtent();
|
||||||
FrameInfo frameInfo{frameIndex, elapsedTime, deltaTime,
|
FrameInfo frameInfo{frameIndex, elapsedTime, deltaTime,
|
||||||
glm::vec2(static_cast<float>(extent.width),
|
glm::vec2(static_cast<float>(extent.width),
|
||||||
static_cast<float>(extent.height))};
|
static_cast<float>(extent.height)),
|
||||||
{
|
this->application.camera};
|
||||||
ZoneScopedN("Application::onUpdate");
|
|
||||||
this->application.onUpdate(frameInfo);
|
this->application.onUpdate(frameInfo);
|
||||||
}
|
this->application.camera.setProjection(
|
||||||
|
-frameInfo.currentFramebufferExtent.x / 2.f,
|
||||||
// Canvas draws in window pixels, origin at the center.
|
|
||||||
// TODO: replace with Debug/Gizmos drawn into scene views through their
|
|
||||||
// cameras.
|
|
||||||
glm::mat4 canvasProjection =
|
|
||||||
glm::ortho(-frameInfo.currentFramebufferExtent.x / 2.f,
|
|
||||||
frameInfo.currentFramebufferExtent.x / 2.f,
|
frameInfo.currentFramebufferExtent.x / 2.f,
|
||||||
-frameInfo.currentFramebufferExtent.y / 2.f,
|
-frameInfo.currentFramebufferExtent.y / 2.f,
|
||||||
frameInfo.currentFramebufferExtent.y / 2.f);
|
frameInfo.currentFramebufferExtent.y / 2.f);
|
||||||
@@ -130,13 +92,41 @@ class Application::Impl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
imguiRenderer.beginFrame();
|
imguiRenderer.beginFrame();
|
||||||
sceneRenderer.beginFrame(renderContext.getFrameIndex());
|
|
||||||
|
|
||||||
panelRegistry.update(frameInfo);
|
this->application.panelRegistry.update(frameInfo);
|
||||||
{
|
|
||||||
ZoneScopedN("Application::onRender");
|
|
||||||
this->application.onRender();
|
this->application.onRender();
|
||||||
}
|
|
||||||
|
renderContext.transitionCurrentImage(
|
||||||
|
commandBuffer, vk::ImageLayout::eColorAttachmentOptimal);
|
||||||
|
|
||||||
|
vk::ClearValue clearValue{};
|
||||||
|
clearValue.color =
|
||||||
|
vk::ClearColorValue{std::array<float, 4>{0.01f, 0.01f, 0.02f, 1.0f}};
|
||||||
|
|
||||||
|
vk::RenderingAttachmentInfo colorAttachment{};
|
||||||
|
colorAttachment.imageView = renderContext.getCurrentImageView();
|
||||||
|
colorAttachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal;
|
||||||
|
colorAttachment.loadOp = vk::AttachmentLoadOp::eClear;
|
||||||
|
colorAttachment.storeOp = vk::AttachmentStoreOp::eStore;
|
||||||
|
colorAttachment.clearValue = clearValue;
|
||||||
|
|
||||||
|
vk::RenderingInfo renderingInfo{};
|
||||||
|
renderingInfo.renderArea = vk::Rect2D{{0, 0}, extent};
|
||||||
|
renderingInfo.layerCount = 1;
|
||||||
|
renderingInfo.colorAttachmentCount = 1;
|
||||||
|
renderingInfo.pColorAttachments = &colorAttachment;
|
||||||
|
|
||||||
|
commandBuffer.beginRendering(renderingInfo);
|
||||||
|
|
||||||
|
vk::Viewport viewport{0.0f,
|
||||||
|
0.0f,
|
||||||
|
static_cast<float>(extent.width),
|
||||||
|
static_cast<float>(extent.height),
|
||||||
|
0.0f,
|
||||||
|
1.0f};
|
||||||
|
vk::Rect2D scissor{{0, 0}, extent};
|
||||||
|
commandBuffer.setViewport(0, viewport);
|
||||||
|
commandBuffer.setScissor(0, scissor);
|
||||||
|
|
||||||
std::vector<ModelRenderSystem::MeshInstance> instances;
|
std::vector<ModelRenderSystem::MeshInstance> instances;
|
||||||
instances.reserve(this->meshes.size() + this->drawCalls.size());
|
instances.reserve(this->meshes.size() + this->drawCalls.size());
|
||||||
@@ -145,124 +135,18 @@ class Application::Impl {
|
|||||||
}
|
}
|
||||||
instances.insert(instances.end(), this->drawCalls.begin(),
|
instances.insert(instances.end(), this->drawCalls.begin(),
|
||||||
this->drawCalls.end());
|
this->drawCalls.end());
|
||||||
|
|
||||||
|
modelRenderSystem.render(commandBuffer, instances, frameInfo);
|
||||||
this->drawCalls.clear();
|
this->drawCalls.clear();
|
||||||
|
|
||||||
RenderGraph graph{renderContext.getTracyContext()};
|
|
||||||
ImageRef backbuffer =
|
|
||||||
graph.importImage(renderContext.getCurrentImage(), Access::Present);
|
|
||||||
|
|
||||||
struct ViewTarget {
|
|
||||||
ImageRef color;
|
|
||||||
ImageRef depth;
|
|
||||||
};
|
|
||||||
std::map<RenderTargetHandle, ViewTarget> viewTargets;
|
|
||||||
// Views without a target render straight into the backbuffer.
|
|
||||||
std::optional<ViewTarget> swapchainTarget;
|
|
||||||
for (SceneViewHandle handle : sceneViewRegistry.GetRenderQueue()) {
|
|
||||||
const SceneView& view = sceneViewRegistry.peek(handle);
|
|
||||||
bool toSwapchain = !view.target.isValid();
|
|
||||||
if (!sceneRegistry.isAlive(view.scene) ||
|
|
||||||
!sceneRegistry.get(view.scene).isVisible() ||
|
|
||||||
(!toSwapchain && !renderTargetPool.isAlive(view.target))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewTarget target;
|
|
||||||
vk::Extent2D targetExtent;
|
|
||||||
// Whether this is the first view this frame to render to its target.
|
|
||||||
bool firstOnTarget = false;
|
|
||||||
if (toSwapchain) {
|
|
||||||
firstOnTarget = !swapchainTarget;
|
|
||||||
if (!swapchainTarget) {
|
|
||||||
swapchainTarget = {
|
|
||||||
backbuffer,
|
|
||||||
graph.importImage(renderContext.getCurrentDepthImage(),
|
|
||||||
Access::DepthWrite)};
|
|
||||||
}
|
|
||||||
target = *swapchainTarget;
|
|
||||||
targetExtent = renderContext.getExtent();
|
|
||||||
} else {
|
|
||||||
RenderTarget& renderTarget = renderTargetPool.get(view.target);
|
|
||||||
auto [it, inserted] = viewTargets.try_emplace(view.target);
|
|
||||||
firstOnTarget = inserted;
|
|
||||||
if (inserted) {
|
|
||||||
it->second = {
|
|
||||||
graph.importImage(renderTarget.getImage(), Access::SampledRead),
|
|
||||||
graph.importImage(renderTarget.getDepthImage(),
|
|
||||||
Access::DepthWrite)};
|
|
||||||
}
|
|
||||||
target = it->second;
|
|
||||||
targetExtent = renderTarget.getExtent();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!firstOnTarget && hasFlag(view.clearFlags, ClearFlags::Color) &&
|
|
||||||
viewsOverwritingOthers.insert(handle).second) {
|
|
||||||
log::warning(
|
|
||||||
"scene view clears its target after another view already "
|
|
||||||
"rendered to it this frame, hiding that view (give it clearFlags "
|
|
||||||
"without Color, or render it first with a lower priority)");
|
|
||||||
}
|
|
||||||
|
|
||||||
const vec4& color = view.clearColor;
|
|
||||||
graph.addPass(
|
|
||||||
"scene view " + std::to_string(handle.index),
|
|
||||||
[&](RenderGraph::PassBuilder& pass) {
|
|
||||||
pass.writeColor(target.color,
|
|
||||||
hasFlag(view.clearFlags, ClearFlags::Color)
|
|
||||||
? vk::AttachmentLoadOp::eClear
|
|
||||||
: vk::AttachmentLoadOp::eLoad,
|
|
||||||
{color.r, color.g, color.b, color.a});
|
|
||||||
pass.writeDepth(target.depth,
|
|
||||||
hasFlag(view.clearFlags, ClearFlags::Depth)
|
|
||||||
? vk::AttachmentLoadOp::eClear
|
|
||||||
: vk::AttachmentLoadOp::eLoad);
|
|
||||||
},
|
|
||||||
[&sceneRenderer, &sceneRegistry, &sceneViewRegistry,
|
|
||||||
&viewsWithoutCamera, handle,
|
|
||||||
targetExtent](vk::CommandBuffer commandBuffer) {
|
|
||||||
ZoneScopedN("scene view pass");
|
|
||||||
const SceneView& view = sceneViewRegistry.peek(handle);
|
|
||||||
Entity camera =
|
|
||||||
view.camera ? view.camera
|
|
||||||
: sceneRegistry.get(view.scene).getActiveCamera();
|
|
||||||
// No camera to look through: the target is just cleared.
|
|
||||||
if (!camera) {
|
|
||||||
if (viewsWithoutCamera.insert(handle).second) {
|
|
||||||
log::warning(
|
|
||||||
"scene view has no camera and its scene has no active "
|
|
||||||
"camera; it will only be cleared (see "
|
|
||||||
"Scene::createCamera)");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sceneRenderer.render(commandBuffer, view, camera, targetExtent);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
graph.addPass(
|
|
||||||
"main",
|
|
||||||
[&](RenderGraph::PassBuilder& pass) {
|
|
||||||
// Keep what scene views drew into the backbuffer.
|
|
||||||
pass.writeColor(backbuffer,
|
|
||||||
swapchainTarget ? vk::AttachmentLoadOp::eLoad
|
|
||||||
: vk::AttachmentLoadOp::eClear,
|
|
||||||
CLEAR_COLOR);
|
|
||||||
for (const auto& [handle, target] : viewTargets) {
|
|
||||||
pass.sample(target.color);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[&](vk::CommandBuffer commandBuffer) {
|
|
||||||
ZoneScopedN("main pass");
|
|
||||||
modelRenderSystem.render(commandBuffer, instances,
|
|
||||||
canvasProjection);
|
|
||||||
|
|
||||||
imguiRenderer.endFrame(commandBuffer);
|
imguiRenderer.endFrame(commandBuffer);
|
||||||
});
|
|
||||||
|
|
||||||
graph.execute(commandBuffer);
|
commandBuffer.endRendering();
|
||||||
|
|
||||||
|
renderContext.transitionCurrentImage(commandBuffer,
|
||||||
|
vk::ImageLayout::ePresentSrcKHR);
|
||||||
|
|
||||||
renderContext.endFrame();
|
renderContext.endFrame();
|
||||||
FrameMark;
|
|
||||||
|
|
||||||
frameIndex++;
|
frameIndex++;
|
||||||
}
|
}
|
||||||
@@ -271,24 +155,21 @@ class Application::Impl {
|
|||||||
|
|
||||||
device.waitIdle();
|
device.waitIdle();
|
||||||
|
|
||||||
this->application._ui.renderer = nullptr;
|
|
||||||
|
|
||||||
auto endTime = std::chrono::high_resolution_clock::now();
|
auto endTime = std::chrono::high_resolution_clock::now();
|
||||||
double totalRuntime =
|
double totalRuntime =
|
||||||
std::chrono::duration<double>(endTime - startTime).count();
|
std::chrono::duration<double>(endTime - startTime).count();
|
||||||
log::info("Application ran for {:.2f}s", totalRuntime);
|
log::info("Application ran for", totalRuntime, "s");
|
||||||
}
|
}
|
||||||
|
|
||||||
void pushMesh(std::vector<CanvasVertex> vertices, std::vector<u32> indices) {
|
void pushMesh(std::vector<CanvasVertex> vertices, std::vector<u32> indices) {
|
||||||
MeshData data;
|
Mesh::Builder builder{};
|
||||||
data.vertices.reserve(vertices.size());
|
builder.vertices.reserve(vertices.size());
|
||||||
for (const CanvasVertex& vertex : vertices) {
|
for (const CanvasVertex& vertex : vertices) {
|
||||||
data.vertices.push_back(
|
builder.vertices.push_back({vertex.position, vertex.color});
|
||||||
{.position = {vertex.position, 0.f}, .color = {vertex.color, 1.f}});
|
|
||||||
}
|
}
|
||||||
data.indices = std::move(indices);
|
builder.indices = std::move(indices);
|
||||||
|
|
||||||
this->meshes.push_back(std::make_unique<Mesh>(this->device, data));
|
this->meshes.push_back(std::make_unique<Mesh>(this->device, builder));
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearMeshes() {
|
void clearMeshes() {
|
||||||
@@ -303,7 +184,7 @@ class Application::Impl {
|
|||||||
this->drawCalls.push_back({&mesh, transform});
|
this->drawCalls.push_back({&mesh, transform});
|
||||||
}
|
}
|
||||||
|
|
||||||
ResourceRegistry& getResourceRegistry() { return resourceRegistry; }
|
Device& getDevice() { return device; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Application& application;
|
Application& application;
|
||||||
@@ -311,7 +192,6 @@ class Application::Impl {
|
|||||||
Window window;
|
Window window;
|
||||||
Device device;
|
Device device;
|
||||||
RenderContext renderContext;
|
RenderContext renderContext;
|
||||||
ResourceRegistry resourceRegistry;
|
|
||||||
|
|
||||||
std::vector<std::unique_ptr<Mesh>> meshes;
|
std::vector<std::unique_ptr<Mesh>> meshes;
|
||||||
std::vector<ModelRenderSystem::MeshInstance> drawCalls;
|
std::vector<ModelRenderSystem::MeshInstance> drawCalls;
|
||||||
@@ -325,17 +205,13 @@ Application::Application(const Config& config)
|
|||||||
: impl{std::make_unique<Impl>(*this, config)},
|
: impl{std::make_unique<Impl>(*this, config)},
|
||||||
canvas{this},
|
canvas{this},
|
||||||
args{std::move(commandLineArgs)},
|
args{std::move(commandLineArgs)},
|
||||||
_ui{ResourceRegistryRef{impl->getResourceRegistry()}},
|
assetRegistry{impl->getDevice()},
|
||||||
_debugUI{this->panelRegistry, this->consoleCommandRegistry} {}
|
_debugUI{this->panelRegistry, this->consoleCommandRegistry} {}
|
||||||
|
|
||||||
Application::~Application() = default;
|
Application::~Application() = default;
|
||||||
|
|
||||||
void Application::run() { impl->run(); }
|
void Application::run() { impl->run(); }
|
||||||
|
|
||||||
ResourceRegistryRef Application::resources() {
|
|
||||||
return ResourceRegistryRef{impl->getResourceRegistry()};
|
|
||||||
}
|
|
||||||
|
|
||||||
void Application::pushMesh(std::vector<CanvasVertex> vertices,
|
void Application::pushMesh(std::vector<CanvasVertex> vertices,
|
||||||
std::vector<u32> indices) {
|
std::vector<u32> indices) {
|
||||||
impl->pushMesh(std::move(vertices), std::move(indices));
|
impl->pushMesh(std::move(vertices), std::move(indices));
|
||||||
@@ -344,7 +220,7 @@ void Application::pushMesh(std::vector<CanvasVertex> vertices,
|
|||||||
void Application::clearMeshes() { impl->clearMeshes(); }
|
void Application::clearMeshes() { impl->clearMeshes(); }
|
||||||
|
|
||||||
void Application::drawMesh(MeshHandle handle, const glm::mat4& transform) {
|
void Application::drawMesh(MeshHandle handle, const glm::mat4& transform) {
|
||||||
impl->drawMesh(impl->getResourceRegistry().meshPool.get(handle), transform);
|
impl->drawMesh(assetRegistry.get<Mesh>(handle), transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#include <sophia/asset_registry.hpp>
|
||||||
|
#include <sophia/util/assert.hpp>
|
||||||
|
|
||||||
|
#include "mesh.hpp"
|
||||||
|
#include "pools/mesh_pool.hpp"
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
class AssetRegistry::Impl {
|
||||||
|
public:
|
||||||
|
explicit Impl(Device& device) : meshPool{device} {}
|
||||||
|
|
||||||
|
MeshPool meshPool;
|
||||||
|
// TexturePool texturePool;
|
||||||
|
// MaterialPool materialPool;
|
||||||
|
};
|
||||||
|
|
||||||
|
AssetRegistry::AssetRegistry(Device& device)
|
||||||
|
: impl{std::make_unique<Impl>(device)} {}
|
||||||
|
|
||||||
|
AssetRegistry::~AssetRegistry() = default;
|
||||||
|
|
||||||
|
template <>
|
||||||
|
MeshHandle AssetRegistry::load<Mesh>(const std::string& path) {
|
||||||
|
(void)path;
|
||||||
|
SOPHIA_ASSERT(false,
|
||||||
|
"Mesh::load(path) is not implemented yet - no model file "
|
||||||
|
"format is supported; build one via Mesh::Builder for now");
|
||||||
|
return MeshHandle{};
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
typename AssetTraits<T>::Handle load(const std::string&) {
|
||||||
|
static_assert(sizeof(T) == 0,
|
||||||
|
"load<T> is not implemented for this asset type");
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
void AssetRegistry::release<Mesh>(MeshHandle handle) {
|
||||||
|
impl->meshPool.release(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
const Mesh& AssetRegistry::get<Mesh>(MeshHandle handle) const {
|
||||||
|
return impl->meshPool.get(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
MeshHandle AssetRegistry::loadMesh(std::vector<CanvasVertex> vertices,
|
||||||
|
std::vector<u32> indices) {
|
||||||
|
Mesh::Builder builder{};
|
||||||
|
builder.vertices.reserve(vertices.size());
|
||||||
|
for (const CanvasVertex& vertex : vertices) {
|
||||||
|
builder.vertices.push_back({vertex.position, vertex.color});
|
||||||
|
}
|
||||||
|
builder.indices = std::move(indices);
|
||||||
|
|
||||||
|
return impl->meshPool.create(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#include "assets/pools/mesh_pool.hpp"
|
||||||
|
|
||||||
|
#include <sophia/util/assert.hpp>
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr u32 GENERATION_MASK = (1u << 12) - 1;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
MeshPool::MeshPool(Device& device) : device{device} {}
|
||||||
|
|
||||||
|
MeshPool::~MeshPool() = default;
|
||||||
|
|
||||||
|
MeshHandle MeshPool::create(const Mesh::Builder& builder) {
|
||||||
|
auto mesh = std::make_unique<Mesh>(device, builder);
|
||||||
|
|
||||||
|
if (!freeList.empty()) {
|
||||||
|
u32 index = freeList.back();
|
||||||
|
freeList.pop_back();
|
||||||
|
slots[index].mesh = std::move(mesh);
|
||||||
|
return MeshHandle{index, slots[index].generation};
|
||||||
|
}
|
||||||
|
|
||||||
|
slots.push_back(Slot{std::move(mesh), 0});
|
||||||
|
return MeshHandle{static_cast<u32>(slots.size() - 1), 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
const Mesh& MeshPool::get(MeshHandle handle) const {
|
||||||
|
SOPHIA_ASSERT(handle.isValid(), "invalid MeshHandle");
|
||||||
|
SOPHIA_ASSERT(handle.index < slots.size(),
|
||||||
|
"MeshHandle index out of range:", handle.index);
|
||||||
|
|
||||||
|
const Slot& slot = slots[handle.index];
|
||||||
|
SOPHIA_ASSERT(slot.generation == handle.generation,
|
||||||
|
"stale MeshHandle (generation mismatch):", handle.index);
|
||||||
|
SOPHIA_ASSERT(slot.mesh != nullptr,
|
||||||
|
"MeshHandle refers to a released mesh:", handle.index);
|
||||||
|
return *slot.mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MeshPool::release(MeshHandle handle) {
|
||||||
|
SOPHIA_ASSERT(handle.isValid(), "invalid MeshHandle");
|
||||||
|
SOPHIA_ASSERT(handle.index < slots.size(),
|
||||||
|
"MeshHandle index out of range:", handle.index);
|
||||||
|
|
||||||
|
Slot& slot = slots[handle.index];
|
||||||
|
SOPHIA_ASSERT(slot.generation == handle.generation,
|
||||||
|
"stale MeshHandle (generation mismatch):", handle.index);
|
||||||
|
SOPHIA_ASSERT(slot.mesh != nullptr,
|
||||||
|
"MeshHandle already released:", handle.index);
|
||||||
|
|
||||||
|
slot.mesh.reset();
|
||||||
|
slot.generation = (slot.generation + 1) & GENERATION_MASK;
|
||||||
|
freeList.push_back(handle.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "device.hpp"
|
||||||
|
#include "mesh.hpp"
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
class MeshPool {
|
||||||
|
public:
|
||||||
|
explicit MeshPool(Device& device);
|
||||||
|
~MeshPool();
|
||||||
|
|
||||||
|
MeshPool(const MeshPool&) = delete;
|
||||||
|
MeshPool& operator=(const MeshPool&) = delete;
|
||||||
|
|
||||||
|
MeshHandle create(const Mesh::Builder& builder);
|
||||||
|
const Mesh& get(MeshHandle handle) const;
|
||||||
|
void release(MeshHandle handle);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Slot {
|
||||||
|
std::unique_ptr<Mesh> mesh;
|
||||||
|
u32 generation = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
Device& device;
|
||||||
|
|
||||||
|
std::vector<Slot> slots;
|
||||||
|
std::vector<u32> freeList;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
* Based on
|
* Based on
|
||||||
* https://github.com/blurrypiano/littleVulkanEngine/blob/main/src/lve_buffer.cpp
|
* https://github.com/blurrypiano/littleVulkanEngine/blob/main/src/lve_buffer.cpp
|
||||||
*/
|
*/
|
||||||
#include "vulkan/buffer.hpp"
|
#include "buffer.hpp"
|
||||||
|
|
||||||
#include <sophia/util/assert.hpp>
|
#include <sophia/util/assert.hpp>
|
||||||
|
|
||||||
@@ -28,29 +28,26 @@ 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,
|
||||||
allocation);
|
memory);
|
||||||
}
|
}
|
||||||
|
|
||||||
Buffer::~Buffer() {
|
Buffer::~Buffer() {
|
||||||
unmap();
|
unmap();
|
||||||
vmaDestroyBuffer(this->device.getAllocator(), this->buffer, this->allocation);
|
this->device.get()->destroyBuffer(this->buffer);
|
||||||
|
this->device.get()->freeMemory(this->memory);
|
||||||
}
|
}
|
||||||
|
|
||||||
vk::Result Buffer::map() {
|
vk::Result Buffer::map(vk::DeviceSize size, vk::DeviceSize offset) {
|
||||||
SOPHIA_ASSERT(buffer && allocation, "Called map on buffer before create");
|
SOPHIA_ASSERT(buffer && memory, "Called map on buffer before create");
|
||||||
|
|
||||||
void* base = nullptr;
|
vk::MemoryMapFlags emptyFlag{};
|
||||||
vk::Result result = static_cast<vk::Result>(
|
return this->device.get()->mapMemory(this->memory, offset, size, emptyFlag,
|
||||||
vmaMapMemory(this->device.getAllocator(), this->allocation, &base));
|
&this->mapped);
|
||||||
if (result == vk::Result::eSuccess) {
|
|
||||||
this->mapped = base;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Buffer::unmap() {
|
void Buffer::unmap() {
|
||||||
if (mapped) {
|
if (mapped) {
|
||||||
vmaUnmapMemory(this->device.getAllocator(), this->allocation);
|
this->device.get()->unmapMemory(this->memory);
|
||||||
mapped = nullptr;
|
mapped = nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,13 +66,19 @@ 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) {
|
||||||
return static_cast<vk::Result>(vmaFlushAllocation(
|
vk::MappedMemoryRange mappedRange = {};
|
||||||
this->device.getAllocator(), this->allocation, offset, size));
|
mappedRange.memory = memory;
|
||||||
|
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) {
|
||||||
return static_cast<vk::Result>(vmaInvalidateAllocation(
|
vk::MappedMemoryRange mappedRange = {};
|
||||||
this->device.getAllocator(), this->allocation, offset, size));
|
mappedRange.memory = memory;
|
||||||
|
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,
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "vulkan/device.hpp"
|
#include "device.hpp"
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
@@ -19,7 +19,8 @@ class Buffer {
|
|||||||
vk::DeviceSize minOffsetAlignment = 1);
|
vk::DeviceSize minOffsetAlignment = 1);
|
||||||
~Buffer();
|
~Buffer();
|
||||||
|
|
||||||
vk::Result map();
|
vk::Result map(vk::DeviceSize size = VK_WHOLE_SIZE,
|
||||||
|
vk::DeviceSize offset = 0);
|
||||||
void unmap();
|
void unmap();
|
||||||
|
|
||||||
void writeToBuffer(void* data, vk::DeviceSize size = VK_WHOLE_SIZE,
|
void writeToBuffer(void* data, vk::DeviceSize size = VK_WHOLE_SIZE,
|
||||||
@@ -40,7 +41,7 @@ class Buffer {
|
|||||||
void* getMappedMemory() const { return mapped; }
|
void* getMappedMemory() const { return mapped; }
|
||||||
uint32_t getInstanceCount() const { return instanceCount; }
|
uint32_t getInstanceCount() const { return instanceCount; }
|
||||||
vk::DeviceSize getInstanceSize() const { return instanceSize; }
|
vk::DeviceSize getInstanceSize() const { return instanceSize; }
|
||||||
vk::DeviceSize getAlignmentSize() const { return alignmentSize; }
|
vk::DeviceSize getAlignmentSize() const { return instanceSize; }
|
||||||
vk::BufferUsageFlags getUsageFlags() const { return usageFlags; }
|
vk::BufferUsageFlags getUsageFlags() const { return usageFlags; }
|
||||||
vk::MemoryPropertyFlags getMemoryPropertyFlags() const {
|
vk::MemoryPropertyFlags getMemoryPropertyFlags() const {
|
||||||
return memoryPropertyFlags;
|
return memoryPropertyFlags;
|
||||||
@@ -54,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;
|
||||||
VmaAllocation allocation = VK_NULL_HANDLE;
|
vk::DeviceMemory memory = VK_NULL_HANDLE;
|
||||||
|
|
||||||
vk::DeviceSize bufferSize;
|
vk::DeviceSize bufferSize;
|
||||||
uint32_t instanceCount;
|
uint32_t instanceCount;
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "vulkan/device.hpp"
|
#include "device.hpp"
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "vulkan/device.hpp"
|
#include "device.hpp"
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
#include "descriptor_pool.hpp"
|
#include "descriptor_pool.hpp"
|
||||||
#include "descriptor_set_layout.hpp"
|
#include "descriptor_set_layout.hpp"
|
||||||
#include "vulkan/device.hpp"
|
#include "device.hpp"
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
#include "vulkan/device.hpp"
|
#include "device.hpp"
|
||||||
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <sophia/util/logger.hpp>
|
#include <sophia/util/logger.hpp>
|
||||||
|
|
||||||
#include "vulkan/debug_label.hpp"
|
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
static VKAPI_ATTR vk::Bool32 VKAPI_CALL
|
static VKAPI_ATTR vk::Bool32 VKAPI_CALL
|
||||||
@@ -20,16 +17,16 @@ debugCallback(vk::DebugUtilsMessageSeverityFlagBitsEXT m_severity,
|
|||||||
(void)pUser_data;
|
(void)pUser_data;
|
||||||
|
|
||||||
if (m_severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eError) {
|
if (m_severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eError) {
|
||||||
log::error("{}", pCallback_data->pMessage);
|
log::error(pCallback_data->pMessage);
|
||||||
return VK_SUCCESS;
|
return VK_SUCCESS;
|
||||||
} else if (m_severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) {
|
} else if (m_severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) {
|
||||||
log::warning("{}", pCallback_data->pMessage);
|
log::warning(pCallback_data->pMessage);
|
||||||
} else if (m_severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eInfo) {
|
} else if (m_severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eInfo) {
|
||||||
log::info("{}", pCallback_data->pMessage);
|
log::info(pCallback_data->pMessage);
|
||||||
} else if (m_severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose) {
|
} else if (m_severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose) {
|
||||||
log::verbose("{}", pCallback_data->pMessage);
|
log::verbose(pCallback_data->pMessage);
|
||||||
} else {
|
} else {
|
||||||
log::info("{}", pCallback_data->pMessage);
|
log::info(pCallback_data->pMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
return VK_FALSE;
|
return VK_FALSE;
|
||||||
@@ -58,76 +55,6 @@ 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 {}: Vulkan 1.3 not supported",
|
|
||||||
properties.deviceName.data());
|
|
||||||
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 {}: missing device feature {}",
|
|
||||||
properties.deviceName.data(), 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*>{}
|
||||||
@@ -140,14 +67,10 @@ 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);
|
||||||
@@ -163,6 +86,22 @@ 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) {
|
||||||
@@ -184,26 +123,35 @@ 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, VmaAllocation& allocation) {
|
vk::Buffer& buffer, vk::DeviceMemory& bufferMemory) {
|
||||||
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;
|
||||||
|
|
||||||
VmaAllocationCreateInfo allocInfo = {};
|
try {
|
||||||
allocInfo.requiredFlags = static_cast<VkMemoryPropertyFlags>(properties);
|
buffer = this->device->createBuffer(bufferInfo);
|
||||||
|
} catch (const vk::SystemError& error) {
|
||||||
VkBuffer rawBuffer = VK_NULL_HANDLE;
|
log::fatal("failed to create vertex buffer");
|
||||||
VkResult result = vmaCreateBuffer(
|
throw std::runtime_error("failed to create vertex buffer");
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer = rawBuffer;
|
vk::MemoryRequirements memoryRequirements =
|
||||||
|
this->device->getBufferMemoryRequirements(buffer);
|
||||||
|
|
||||||
|
vk::MemoryAllocateInfo allocInfo = {};
|
||||||
|
allocInfo.allocationSize = memoryRequirements.size;
|
||||||
|
allocInfo.memoryTypeIndex =
|
||||||
|
findMemoryType(memoryRequirements.memoryTypeBits, properties);
|
||||||
|
|
||||||
|
try {
|
||||||
|
bufferMemory = this->device->allocateMemory(allocInfo);
|
||||||
|
} catch (const vk::SystemError& error) {
|
||||||
|
log::fatal("failed to allocate vertex buffer memory");
|
||||||
|
throw std::runtime_error("failed to allocate vertex buffer memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
this->device->bindBufferMemory(buffer, bufferMemory, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
vk::CommandBuffer Device::beginSingleTimeCommands() {
|
vk::CommandBuffer Device::beginSingleTimeCommands() {
|
||||||
@@ -221,7 +169,7 @@ vk::CommandBuffer Device::beginSingleTimeCommands() {
|
|||||||
throw vk::SystemError(result);
|
throw vk::SystemError(result);
|
||||||
}
|
}
|
||||||
} catch (const vk::SystemError& error) {
|
} catch (const vk::SystemError& error) {
|
||||||
log::error("failed to allocate single time commandBuffer. Error: {}",
|
log::error("failed to allocate single time commandBuffer. Error: ",
|
||||||
error.what());
|
error.what());
|
||||||
throw std::runtime_error("failed to allocate single time commandBuffer");
|
throw std::runtime_error("failed to allocate single time commandBuffer");
|
||||||
}
|
}
|
||||||
@@ -235,7 +183,7 @@ vk::CommandBuffer Device::beginSingleTimeCommands() {
|
|||||||
throw vk::SystemError(result);
|
throw vk::SystemError(result);
|
||||||
}
|
}
|
||||||
} catch (const vk::SystemError& error) {
|
} catch (const vk::SystemError& error) {
|
||||||
log::error("failed to begin single time commandBuffer. Error: {}",
|
log::error("failed to begin single time commandBuffer. Error: ",
|
||||||
error.what());
|
error.what());
|
||||||
throw std::runtime_error("failed to begin single time commandBuffer");
|
throw std::runtime_error("failed to begin single time commandBuffer");
|
||||||
}
|
}
|
||||||
@@ -270,21 +218,43 @@ 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, VmaAllocation& allocation) {
|
vk::Image& image,
|
||||||
VmaAllocationCreateInfo allocInfo = {};
|
vk::DeviceMemory& imageMemory) {
|
||||||
allocInfo.requiredFlags = static_cast<VkMemoryPropertyFlags>(properties);
|
try {
|
||||||
|
vk::Result result = this->device->createImage(&imageInfo, nullptr, &image);
|
||||||
VkImage rawImage = VK_NULL_HANDLE;
|
if (result != vk::Result::eSuccess) {
|
||||||
VkResult result = vmaCreateImage(
|
throw vk::SystemError(result);
|
||||||
this->allocator, &static_cast<const VkImageCreateInfo&>(imageInfo),
|
}
|
||||||
&allocInfo, &rawImage, &allocation, nullptr);
|
} catch (const vk::SystemError& error) {
|
||||||
if (result != VK_SUCCESS) {
|
log::fatal("failed to create image. Error: ", error.what());
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
image = rawImage;
|
vk::MemoryRequirements memoryRequirements =
|
||||||
|
this->device->getImageMemoryRequirements(image);
|
||||||
|
|
||||||
|
vk::MemoryAllocateInfo allocateInfo{};
|
||||||
|
allocateInfo.allocationSize = memoryRequirements.size;
|
||||||
|
allocateInfo.memoryTypeIndex =
|
||||||
|
findMemoryType(memoryRequirements.memoryTypeBits, properties);
|
||||||
|
|
||||||
|
try {
|
||||||
|
vk::Result result =
|
||||||
|
this->device->allocateMemory(&allocateInfo, nullptr, &imageMemory);
|
||||||
|
if (result != vk::Result::eSuccess) {
|
||||||
|
throw vk::SystemError(result);
|
||||||
|
}
|
||||||
|
} catch (const vk::SystemError& error) {
|
||||||
|
log::fatal("failed to allocate image memory. Error: ", error.what());
|
||||||
|
throw std::runtime_error("failed to allocate image memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this->device->bindImageMemory(image, imageMemory, 0);
|
||||||
|
} catch (const vk::SystemError& error) {
|
||||||
|
log::fatal("failed to bind image memory. Error: ", error.what());
|
||||||
|
throw std::runtime_error("failed to bind image memory");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Device::populateImGuiInitInfo(ImGui_ImplVulkan_InitInfo& initInfo) {
|
void Device::populateImGuiInitInfo(ImGui_ImplVulkan_InitInfo& initInfo) {
|
||||||
@@ -318,8 +288,6 @@ void Device::setupDebugMessenger() {
|
|||||||
nullptr, &this->debugMessenger) != VK_SUCCESS) {
|
nullptr, &this->debugMessenger) != VK_SUCCESS) {
|
||||||
throw std::runtime_error("failed to set up debug callback!");
|
throw std::runtime_error("failed to set up debug callback!");
|
||||||
}
|
}
|
||||||
|
|
||||||
loadDebugLabelFunctions(*instance);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Device::checkValidationLayerSupport() {
|
bool Device::checkValidationLayerSupport() {
|
||||||
@@ -375,7 +343,7 @@ std::vector<const char*> Device::getRequiredExtensions() {
|
|||||||
throw std::exception();
|
throw std::exception();
|
||||||
}
|
}
|
||||||
|
|
||||||
log::verbose("Number of available extensions: {}", available_extension_count);
|
log::verbose("Number of available extensions: ", available_extension_count);
|
||||||
|
|
||||||
log::verbose("Available extensions:");
|
log::verbose("Available extensions:");
|
||||||
for (const auto& e : available_extensions) {
|
for (const auto& e : available_extensions) {
|
||||||
@@ -457,10 +425,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -483,7 +447,7 @@ void Device::pickPhysicalDevice() {
|
|||||||
log::fatal("Failed to find any physical devices.");
|
log::fatal("Failed to find any physical devices.");
|
||||||
throw std::exception();
|
throw std::exception();
|
||||||
}
|
}
|
||||||
log::verbose("Physical Devices count: {}", physicalDevices.size());
|
log::verbose("Physical Devices count: ", physicalDevices.size());
|
||||||
|
|
||||||
// Select first physical device that is suitable
|
// Select first physical device that is suitable
|
||||||
for (const auto& device : physicalDevices) {
|
for (const auto& device : physicalDevices) {
|
||||||
@@ -499,7 +463,7 @@ void Device::pickPhysicalDevice() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this->properties = this->physicalDevice.getProperties();
|
this->properties = this->physicalDevice.getProperties();
|
||||||
log::verbose("Physical Device: {}", this->properties.deviceName.data());
|
log::verbose("Physical Device: ", this->properties.deviceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Device::createLogicalDevice() {
|
void Device::createLogicalDevice() {
|
||||||
@@ -518,17 +482,15 @@ void Device::createLogicalDevice() {
|
|||||||
{vk::DeviceQueueCreateFlags(), queueFamily, 1, &queuePriority});
|
{vk::DeviceQueueCreateFlags(), queueFamily, 1, &queuePriority});
|
||||||
}
|
}
|
||||||
|
|
||||||
DeviceFeatureChain enabledFeatures;
|
vk::PhysicalDeviceVulkan13Features vulkan13Features{};
|
||||||
forEachRequiredFeature(enabledFeatures, [](vk::Bool32& feature, const char*) {
|
vulkan13Features.dynamicRendering = vk::True;
|
||||||
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());
|
||||||
// Core features are passed through PhysicalDeviceFeatures2 in the pNext
|
createInfo.pEnabledFeatures = &deviceFeatures;
|
||||||
// chain, so pEnabledFeatures must stay null.
|
createInfo.pNext = &vulkan13Features;
|
||||||
createInfo.pNext = &enabledFeatures.get<vk::PhysicalDeviceFeatures2>();
|
|
||||||
|
|
||||||
createInfo.enabledExtensionCount =
|
createInfo.enabledExtensionCount =
|
||||||
static_cast<uint32_t>(this->enabledExtensions.size());
|
static_cast<uint32_t>(this->enabledExtensions.size());
|
||||||
@@ -548,21 +510,6 @@ 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,7 +1,6 @@
|
|||||||
#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>
|
||||||
@@ -48,8 +47,6 @@ class Device {
|
|||||||
|
|
||||||
void waitIdle() { this->device->waitIdle(); }
|
void waitIdle() { this->device->waitIdle(); }
|
||||||
|
|
||||||
vk::PhysicalDevice getPhysicalDevice() const { return this->physicalDevice; }
|
|
||||||
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; }
|
||||||
@@ -63,13 +60,15 @@ 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,
|
||||||
VmaAllocation& allocation);
|
vk::DeviceMemory& bufferMemory);
|
||||||
|
|
||||||
vk::CommandBuffer beginSingleTimeCommands();
|
vk::CommandBuffer beginSingleTimeCommands();
|
||||||
|
|
||||||
@@ -80,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,
|
||||||
VmaAllocation& allocation);
|
vk::DeviceMemory& imageMemory);
|
||||||
|
|
||||||
void populateImGuiInitInfo(ImGui_ImplVulkan_InitInfo& initInfo);
|
void populateImGuiInitInfo(ImGui_ImplVulkan_InitInfo& initInfo);
|
||||||
|
|
||||||
@@ -101,7 +100,6 @@ 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;
|
||||||
@@ -113,8 +111,6 @@ 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;
|
||||||
|
|
||||||
@@ -1,66 +1,82 @@
|
|||||||
#include "vulkan/image.hpp"
|
#include "image.hpp"
|
||||||
|
|
||||||
#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 "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 {
|
|
||||||
return vk::ImageSubresourceRange{this->aspectMask, 0, this->mipLevels, 0,
|
|
||||||
this->arrayLayers};
|
|
||||||
}
|
|
||||||
|
|
||||||
AccessState Image::getAccessState() const {
|
|
||||||
AccessState state = getStateForLayout(this->layout);
|
|
||||||
|
|
||||||
// A wrapped (swapchain) image is handed to us by the acquire semaphore, so
|
|
||||||
// whatever we do next has to be ordered after that wait.
|
|
||||||
if (this->isExternal() && (this->layout == vk::ImageLayout::eUndefined ||
|
|
||||||
this->layout == vk::ImageLayout::ePresentSrcKHR)) {
|
|
||||||
state.stage = SWAPCHAIN_ACQUIRE_STAGE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Image::transitionLayout(vk::CommandBuffer commandBuffer,
|
void Image::transitionLayout(vk::CommandBuffer commandBuffer,
|
||||||
vk::ImageLayout newLayout) {
|
vk::ImageLayout newLayout) {
|
||||||
AccessState from = this->getAccessState();
|
vk::ImageMemoryBarrier barrier{};
|
||||||
AccessState to = getStateForLayout(newLayout);
|
barrier.oldLayout = this->layout;
|
||||||
|
barrier.newLayout = newLayout;
|
||||||
|
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||||
|
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||||
|
barrier.image = this->get();
|
||||||
|
barrier.subresourceRange.aspectMask = this->aspectMask;
|
||||||
|
barrier.subresourceRange.baseMipLevel = 0;
|
||||||
|
barrier.subresourceRange.levelCount = this->mipLevels;
|
||||||
|
barrier.subresourceRange.baseArrayLayer = 0;
|
||||||
|
barrier.subresourceRange.layerCount = this->arrayLayers;
|
||||||
|
|
||||||
if (needsBarrier(from, to)) {
|
vk::PipelineStageFlags srcStage;
|
||||||
vk::ImageMemoryBarrier2 barrier =
|
vk::PipelineStageFlags dstStage;
|
||||||
makeImageBarrier(this->get(), this->getSubresourceRange(), from, to);
|
|
||||||
recordImageBarriers(commandBuffer, barrier);
|
if (this->layout == vk::ImageLayout::eUndefined &&
|
||||||
|
newLayout == vk::ImageLayout::eTransferDstOptimal) {
|
||||||
|
barrier.srcAccessMask = {};
|
||||||
|
barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite;
|
||||||
|
srcStage = vk::PipelineStageFlagBits::eTopOfPipe;
|
||||||
|
dstStage = vk::PipelineStageFlagBits::eTransfer;
|
||||||
|
} else if (this->layout == vk::ImageLayout::eTransferDstOptimal &&
|
||||||
|
newLayout == vk::ImageLayout::eShaderReadOnlyOptimal) {
|
||||||
|
barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
|
||||||
|
barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
|
||||||
|
srcStage = vk::PipelineStageFlagBits::eTransfer;
|
||||||
|
dstStage = vk::PipelineStageFlagBits::eFragmentShader;
|
||||||
|
} else if (this->layout == vk::ImageLayout::eUndefined &&
|
||||||
|
newLayout == vk::ImageLayout::eDepthStencilAttachmentOptimal) {
|
||||||
|
barrier.srcAccessMask = {};
|
||||||
|
barrier.dstAccessMask = vk::AccessFlagBits::eDepthStencilAttachmentRead |
|
||||||
|
vk::AccessFlagBits::eDepthStencilAttachmentWrite;
|
||||||
|
srcStage = vk::PipelineStageFlagBits::eTopOfPipe;
|
||||||
|
dstStage = vk::PipelineStageFlagBits::eEarlyFragmentTests;
|
||||||
|
} else if (this->layout == vk::ImageLayout::eUndefined &&
|
||||||
|
newLayout == vk::ImageLayout::eColorAttachmentOptimal) {
|
||||||
|
barrier.srcAccessMask = {};
|
||||||
|
barrier.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
|
||||||
|
srcStage = vk::PipelineStageFlagBits::eTopOfPipe;
|
||||||
|
dstStage = vk::PipelineStageFlagBits::eColorAttachmentOutput;
|
||||||
|
} else if (this->layout == vk::ImageLayout::eUndefined &&
|
||||||
|
newLayout == vk::ImageLayout::eGeneral) {
|
||||||
|
barrier.srcAccessMask = {};
|
||||||
|
barrier.dstAccessMask =
|
||||||
|
vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite;
|
||||||
|
srcStage = vk::PipelineStageFlagBits::eTopOfPipe;
|
||||||
|
dstStage = vk::PipelineStageFlagBits::eComputeShader;
|
||||||
|
} else if (this->layout == vk::ImageLayout::eColorAttachmentOptimal &&
|
||||||
|
newLayout == vk::ImageLayout::ePresentSrcKHR) {
|
||||||
|
barrier.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
|
||||||
|
barrier.dstAccessMask = {};
|
||||||
|
srcStage = vk::PipelineStageFlagBits::eColorAttachmentOutput;
|
||||||
|
dstStage = vk::PipelineStageFlagBits::eBottomOfPipe;
|
||||||
|
} else if (this->layout == vk::ImageLayout::ePresentSrcKHR &&
|
||||||
|
newLayout == vk::ImageLayout::eColorAttachmentOptimal) {
|
||||||
|
barrier.srcAccessMask = {};
|
||||||
|
barrier.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
|
||||||
|
srcStage = vk::PipelineStageFlagBits::eTopOfPipe;
|
||||||
|
dstStage = vk::PipelineStageFlagBits::eColorAttachmentOutput;
|
||||||
|
} else {
|
||||||
|
log::fatal("Unsupported image layout transition.");
|
||||||
|
throw std::runtime_error("Unsupported image layout transition.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
commandBuffer.pipelineBarrier(srcStage, dstStage, {}, nullptr, nullptr,
|
||||||
|
barrier);
|
||||||
|
|
||||||
this->layout = newLayout;
|
this->layout = newLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,26 +233,35 @@ 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;
|
||||||
|
|
||||||
this->device.createImageWithInfo(imageInfo, this->memoryProperties,
|
result->image = this->device.get()->createImageUnique(imageInfo);
|
||||||
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;
|
||||||
VmaAllocation stagingAllocation;
|
vk::DeviceMemory stagingMemory;
|
||||||
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, stagingAllocation);
|
stagingBuffer, stagingMemory);
|
||||||
|
|
||||||
void* mapped = nullptr;
|
void* mapped =
|
||||||
vmaMapMemory(allocator, stagingAllocation, &mapped);
|
this->device.get()->mapMemory(stagingMemory, 0, this->initialDataSize);
|
||||||
memcpy(mapped, this->initialData,
|
memcpy(mapped, this->initialData,
|
||||||
static_cast<size_t>(this->initialDataSize));
|
static_cast<size_t>(this->initialDataSize));
|
||||||
vmaUnmapMemory(allocator, stagingAllocation);
|
this->device.get()->unmapMemory(stagingMemory);
|
||||||
|
|
||||||
vk::BufferImageCopy region{};
|
vk::BufferImageCopy region{};
|
||||||
region.bufferOffset = 0;
|
region.bufferOffset = 0;
|
||||||
@@ -261,7 +286,8 @@ std::unique_ptr<Image> Image::Builder::build() {
|
|||||||
}
|
}
|
||||||
this->device.endSingleTimeCommands(commandBuffer);
|
this->device.endSingleTimeCommands(commandBuffer);
|
||||||
|
|
||||||
vmaDestroyBuffer(allocator, stagingBuffer, stagingAllocation);
|
this->device.get()->destroyBuffer(stagingBuffer);
|
||||||
|
this->device.get()->freeMemory(stagingMemory);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->createView) {
|
if (this->createView) {
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
#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>
|
||||||
|
|
||||||
#include "vulkan/access.hpp"
|
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
class Device;
|
class Device;
|
||||||
@@ -18,12 +14,11 @@ class Image {
|
|||||||
|
|
||||||
Image(const Image&) = delete;
|
Image(const Image&) = delete;
|
||||||
Image& operator=(const Image&) = delete;
|
Image& operator=(const Image&) = delete;
|
||||||
Image(Image&& other) noexcept;
|
Image(Image&&) = default;
|
||||||
Image& operator=(Image&&) = delete;
|
Image& operator=(Image&&) = delete;
|
||||||
~Image();
|
|
||||||
|
|
||||||
vk::Image get() const {
|
vk::Image get() const {
|
||||||
return this->image ? this->image : this->externalImage;
|
return this->image ? this->image.get() : 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(); }
|
||||||
@@ -33,20 +28,6 @@ class Image {
|
|||||||
vk::Format getFormat() const { return this->format; }
|
vk::Format getFormat() const { return this->format; }
|
||||||
vk::Extent3D getExtent() const { return this->extent; }
|
vk::Extent3D getExtent() const { return this->extent; }
|
||||||
vk::ImageLayout getLayout() const { return this->layout; }
|
vk::ImageLayout getLayout() const { return this->layout; }
|
||||||
vk::ImageSubresourceRange getSubresourceRange() const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The layout this image was last left in, plus the stages and accesses that
|
|
||||||
* a barrier out of it has to wait on.
|
|
||||||
*/
|
|
||||||
AccessState getAccessState() const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the tracked layout without recording a barrier. For code that
|
|
||||||
* does its own transitions (the RenderGraph) and needs to hand the final
|
|
||||||
* layout back.
|
|
||||||
*/
|
|
||||||
void recordLayout(vk::ImageLayout newLayout) { this->layout = newLayout; }
|
|
||||||
|
|
||||||
void transitionLayout(vk::CommandBuffer commandBuffer,
|
void transitionLayout(vk::CommandBuffer commandBuffer,
|
||||||
vk::ImageLayout newLayout);
|
vk::ImageLayout newLayout);
|
||||||
@@ -59,8 +40,8 @@ class Image {
|
|||||||
Device& device;
|
Device& device;
|
||||||
|
|
||||||
vk::Image externalImage;
|
vk::Image externalImage;
|
||||||
vk::Image image;
|
vk::UniqueImage image;
|
||||||
VmaAllocation allocation = VK_NULL_HANDLE;
|
vk::UniqueDeviceMemory memory;
|
||||||
vk::UniqueImageView view;
|
vk::UniqueImageView view;
|
||||||
vk::UniqueSampler sampler;
|
vk::UniqueSampler sampler;
|
||||||
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#include "imgui_renderer.hpp"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
#include <imgui_impl_glfw.h>
|
||||||
|
#include <imgui_impl_vulkan.h>
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
ImGuiRenderer::ImGuiRenderer(Window& window, Device& device,
|
||||||
|
vk::Format colorAttachmentFormat,
|
||||||
|
u32 minImageCount) {
|
||||||
|
// 1. Construct ImGui context
|
||||||
|
IMGUI_CHECKVERSION();
|
||||||
|
ImGui::CreateContext();
|
||||||
|
|
||||||
|
ImGui_ImplGlfw_InitForVulkan(window.getGLFWwindow(), true);
|
||||||
|
|
||||||
|
ImGui_ImplVulkan_InitInfo initInfo = {};
|
||||||
|
device.populateImGuiInitInfo(initInfo);
|
||||||
|
initInfo.DescriptorPoolSize =
|
||||||
|
IMGUI_IMPL_VULKAN_MINIMUM_SAMPLED_IMAGE_POOL_SIZE + 5;
|
||||||
|
initInfo.MinImageCount = minImageCount;
|
||||||
|
initInfo.ImageCount = minImageCount;
|
||||||
|
|
||||||
|
VkFormat rawColorAttachmentFormat =
|
||||||
|
static_cast<VkFormat>(colorAttachmentFormat);
|
||||||
|
initInfo.UseDynamicRendering = true;
|
||||||
|
initInfo.PipelineInfoMain.PipelineRenderingCreateInfo.sType =
|
||||||
|
VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR;
|
||||||
|
initInfo.PipelineInfoMain.PipelineRenderingCreateInfo.colorAttachmentCount =
|
||||||
|
1;
|
||||||
|
initInfo.PipelineInfoMain.PipelineRenderingCreateInfo
|
||||||
|
.pColorAttachmentFormats = &rawColorAttachmentFormat;
|
||||||
|
|
||||||
|
ImGui_ImplVulkan_Init(&initInfo);
|
||||||
|
|
||||||
|
// 2. Enable dark theme
|
||||||
|
ImGui::StyleColorsDark();
|
||||||
|
|
||||||
|
// 3. Enable docking / keyboard
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||||
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||||
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When viewports are enabled we tweak WindowRounding/WindowBg so
|
||||||
|
* platform windows can look identical to regular ones.
|
||||||
|
*/
|
||||||
|
ImGuiStyle& style = ImGui::GetStyle();
|
||||||
|
if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
|
||||||
|
style.WindowRounding = 0.0f;
|
||||||
|
style.Colors[ImGuiCol_WindowBg].w = 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiRenderer::~ImGuiRenderer() {
|
||||||
|
ImGui_ImplVulkan_Shutdown();
|
||||||
|
ImGui_ImplGlfw_Shutdown();
|
||||||
|
ImGui::DestroyContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGuiRenderer::beginFrame() {
|
||||||
|
ImGui_ImplVulkan_NewFrame();
|
||||||
|
ImGui_ImplGlfw_NewFrame();
|
||||||
|
ImGui::NewFrame();
|
||||||
|
|
||||||
|
ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport(),
|
||||||
|
ImGuiDockNodeFlags_PassthruCentralNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImGuiRenderer::endFrame(vk::CommandBuffer commandBuffer) {
|
||||||
|
ImGui::Render();
|
||||||
|
ImDrawData* mainDrawData = ImGui::GetDrawData();
|
||||||
|
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
|
||||||
|
ImGui::UpdatePlatformWindows();
|
||||||
|
ImGui::RenderPlatformWindowsDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui_ImplVulkan_RenderDrawData(mainDrawData, commandBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <sophia/types.hpp>
|
||||||
|
|
||||||
|
#include "device.hpp"
|
||||||
|
#include "window.hpp"
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
class ImGuiRenderer {
|
||||||
|
public:
|
||||||
|
ImGuiRenderer(const ImGuiRenderer&) = delete;
|
||||||
|
ImGuiRenderer& operator=(const ImGuiRenderer&) = delete;
|
||||||
|
|
||||||
|
ImGuiRenderer(Window& window, Device& device,
|
||||||
|
vk::Format colorAttachmentFormat, u32 minImageCount);
|
||||||
|
~ImGuiRenderer();
|
||||||
|
|
||||||
|
void beginFrame();
|
||||||
|
void endFrame(vk::CommandBuffer commandBuffer);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
@@ -1,36 +1,39 @@
|
|||||||
#include "resources/mesh.hpp"
|
#include "mesh.hpp"
|
||||||
|
|
||||||
#include <sophia/util/assert.hpp>
|
#include <sophia/util/assert.hpp>
|
||||||
#include <sophia/util/logger.hpp>
|
#include <sophia/util/logger.hpp>
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
std::vector<vk::VertexInputBindingDescription> Mesh::getBindingDescriptions() {
|
std::vector<vk::VertexInputBindingDescription>
|
||||||
return {{0, sizeof(MeshVertex), vk::VertexInputRate::eVertex}};
|
Mesh::Vertex::getBindingDescriptions() {
|
||||||
|
std::vector<vk::VertexInputBindingDescription> bindingDescriptions(1);
|
||||||
|
bindingDescriptions[0].binding = 0;
|
||||||
|
bindingDescriptions[0].stride = sizeof(Vertex);
|
||||||
|
bindingDescriptions[0].inputRate = vk::VertexInputRate::eVertex;
|
||||||
|
|
||||||
|
return bindingDescriptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<vk::VertexInputAttributeDescription>
|
std::vector<vk::VertexInputAttributeDescription>
|
||||||
Mesh::getAttributeDescriptions() {
|
Mesh::Vertex::getAttributeDescriptions() {
|
||||||
return {
|
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions{};
|
||||||
{0, 0, vk::Format::eR32G32B32Sfloat, offsetof(MeshVertex, position)},
|
|
||||||
{1, 0, vk::Format::eR32G32B32Sfloat, offsetof(MeshVertex, normal)},
|
attributeDescriptions.push_back(
|
||||||
{2, 0, vk::Format::eR32G32Sfloat, offsetof(MeshVertex, uv)},
|
{0, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, position)});
|
||||||
{3, 0, vk::Format::eR32G32B32A32Sfloat, offsetof(MeshVertex, color)},
|
attributeDescriptions.push_back(
|
||||||
};
|
{1, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, color)});
|
||||||
|
// attributeDescriptions.push_back(
|
||||||
|
// {2, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, normal)});
|
||||||
|
// attributeDescriptions.push_back(
|
||||||
|
// {3, 0, vk::Format::eR32G32Sfloat, offsetof(Vertex, uv)});
|
||||||
|
|
||||||
|
return attributeDescriptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
Mesh::Mesh(Device& device, const MeshData& data) : device{device} {
|
Mesh::Mesh(Device& device, const Builder& builder) : device{device} {
|
||||||
if (!data.vertices.empty()) {
|
createVertexBuffers(builder.vertices);
|
||||||
this->bounds = {data.vertices.front().position,
|
createIndexBuffers(builder.indices);
|
||||||
data.vertices.front().position};
|
|
||||||
for (const MeshVertex& vertex : data.vertices) {
|
|
||||||
this->bounds.min = glm::min(this->bounds.min, vertex.position);
|
|
||||||
this->bounds.max = glm::max(this->bounds.max, vertex.position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createVertexBuffers(data.vertices);
|
|
||||||
createIndexBuffers(data.indices);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Mesh::~Mesh() = default;
|
Mesh::~Mesh() = default;
|
||||||
@@ -46,17 +49,15 @@ void Mesh::bind(vk::CommandBuffer commandBuffer) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mesh::draw(vk::CommandBuffer commandBuffer, u32 instanceCount,
|
void Mesh::draw(vk::CommandBuffer commandBuffer) const {
|
||||||
u32 firstInstance) const {
|
|
||||||
if (this->hasIndexBuffer) {
|
if (this->hasIndexBuffer) {
|
||||||
commandBuffer.drawIndexed(this->indexCount, instanceCount, 0, 0,
|
commandBuffer.drawIndexed(this->indexCount, 1, 0, 0, 0);
|
||||||
firstInstance);
|
|
||||||
} else {
|
} else {
|
||||||
commandBuffer.draw(this->vertexCount, instanceCount, 0, firstInstance);
|
commandBuffer.draw(this->vertexCount, 1, 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mesh::createVertexBuffers(const std::vector<MeshVertex>& vertices) {
|
void Mesh::createVertexBuffers(const std::vector<Vertex>& vertices) {
|
||||||
this->vertexCount = static_cast<u32>(vertices.size());
|
this->vertexCount = static_cast<u32>(vertices.size());
|
||||||
SOPHIA_ASSERT(this->vertexCount >= 3, "vertex count must be at least 3");
|
SOPHIA_ASSERT(this->vertexCount >= 3, "vertex count must be at least 3");
|
||||||
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <sophia/asset_registry.hpp>
|
||||||
|
#include <sophia/types.hpp>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#define GLM_FORCE_RADIANS
|
||||||
|
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
#include "buffer.hpp"
|
||||||
|
#include "device.hpp"
|
||||||
|
|
||||||
|
namespace sophia {
|
||||||
|
|
||||||
|
class Mesh {
|
||||||
|
public:
|
||||||
|
using HandleType = MeshHandle;
|
||||||
|
|
||||||
|
struct Vertex {
|
||||||
|
glm::vec2 position{};
|
||||||
|
glm::vec3 color{};
|
||||||
|
// glm::vec3 normal{};
|
||||||
|
// glm::vec2 uv{};
|
||||||
|
|
||||||
|
static std::vector<vk::VertexInputBindingDescription>
|
||||||
|
getBindingDescriptions();
|
||||||
|
static std::vector<vk::VertexInputAttributeDescription>
|
||||||
|
getAttributeDescriptions();
|
||||||
|
|
||||||
|
// bool operator==(const Vertex& other) const {
|
||||||
|
// return position == other.position && color == other.color;
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Builder {
|
||||||
|
std::vector<Vertex> vertices{};
|
||||||
|
std::vector<uint32_t> indices{};
|
||||||
|
|
||||||
|
// void loadModel(const std::string& filepath);
|
||||||
|
};
|
||||||
|
|
||||||
|
Mesh(Device& device, const Mesh::Builder& builder);
|
||||||
|
~Mesh();
|
||||||
|
|
||||||
|
Mesh(const Mesh&) = delete;
|
||||||
|
Mesh& operator=(const Mesh&) = delete;
|
||||||
|
|
||||||
|
// static std::unique_ptr<Mesh> createMeshFromFile(
|
||||||
|
// Device& device, const std::string& filepath);
|
||||||
|
|
||||||
|
void bind(vk::CommandBuffer commandBuffer) const;
|
||||||
|
void draw(vk::CommandBuffer commandBuffer) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void createVertexBuffers(const std::vector<Vertex>& vertices);
|
||||||
|
void createIndexBuffers(const std::vector<u32>& indices);
|
||||||
|
|
||||||
|
Device& device;
|
||||||
|
|
||||||
|
std::unique_ptr<Buffer> vertexBuffer;
|
||||||
|
u32 vertexCount;
|
||||||
|
|
||||||
|
bool hasIndexBuffer = false;
|
||||||
|
std::unique_ptr<Buffer> indexBuffer;
|
||||||
|
u32 indexCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sophia
|
||||||
+2
-2
@@ -3,9 +3,9 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
|
|
||||||
|
#include "device.hpp"
|
||||||
#include "pipeline.hpp"
|
#include "pipeline.hpp"
|
||||||
#include "vulkan/device.hpp"
|
#include "shader.hpp"
|
||||||
#include "vulkan/shader.hpp"
|
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
+22
-50
@@ -95,27 +95,6 @@ GraphicsPipeline::Builder& GraphicsPipeline::Builder::setVertexInput(
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
GraphicsPipeline::Builder& GraphicsPipeline::Builder::setAlphaBlending(
|
|
||||||
bool enabled) {
|
|
||||||
this->alphaBlending = enabled;
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
GraphicsPipeline::Builder& GraphicsPipeline::Builder::setDepthTest(
|
|
||||||
bool test, bool write, vk::CompareOp compareOp) {
|
|
||||||
this->depthTest = test;
|
|
||||||
this->depthWrite = write;
|
|
||||||
this->depthCompareOp = compareOp;
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
GraphicsPipeline::Builder& GraphicsPipeline::Builder::setCullMode(
|
|
||||||
vk::CullModeFlags cullMode, vk::FrontFace frontFace) {
|
|
||||||
this->cullMode = cullMode;
|
|
||||||
this->frontFace = frontFace;
|
|
||||||
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(
|
||||||
@@ -145,41 +124,34 @@ std::unique_ptr<GraphicsPipeline> GraphicsPipeline::Builder::build() const {
|
|||||||
"setDepthAttachmentFormat() was not called");
|
"setDepthAttachmentFormat() was not called");
|
||||||
}
|
}
|
||||||
|
|
||||||
return std::unique_ptr<GraphicsPipeline>(new GraphicsPipeline(*this));
|
return std::unique_ptr<GraphicsPipeline>(new GraphicsPipeline(
|
||||||
|
this->device, *this->vertexShader, *this->fragmentShader,
|
||||||
|
this->pipelineLayout, this->colorAttachmentFormats,
|
||||||
|
this->depthAttachmentFormat, this->stencilAttachmentFormat,
|
||||||
|
this->bindingDescriptions, this->attributeDescriptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
GraphicsPipeline::GraphicsPipeline(const Builder& builder)
|
GraphicsPipeline::GraphicsPipeline(
|
||||||
: Pipeline(builder.device, vk::PipelineBindPoint::eGraphics) {
|
Device& device, Shader& vertexShader, Shader& fragmentShader,
|
||||||
|
vk::PipelineLayout pipelineLayout,
|
||||||
|
std::vector<vk::Format> colorAttachmentFormats,
|
||||||
|
vk::Format depthAttachmentFormat, vk::Format stencilAttachmentFormat,
|
||||||
|
std::vector<vk::VertexInputBindingDescription> bindingDescriptions,
|
||||||
|
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions)
|
||||||
|
: Pipeline(device, vk::PipelineBindPoint::eGraphics) {
|
||||||
setDefaultGraphicsPipelineConfig();
|
setDefaultGraphicsPipelineConfig();
|
||||||
|
|
||||||
if (builder.alphaBlending) {
|
this->config.bindingDescriptions = std::move(bindingDescriptions);
|
||||||
vk::PipelineColorBlendAttachmentState& blend =
|
this->config.attributeDescriptions = std::move(attributeDescriptions);
|
||||||
this->config.colorBlendAttachment;
|
this->config.pipelineLayout = pipelineLayout;
|
||||||
blend.blendEnable = vk::True;
|
this->config.colorAttachmentFormats = std::move(colorAttachmentFormats);
|
||||||
blend.srcColorBlendFactor = vk::BlendFactor::eSrcAlpha;
|
this->config.depthAttachmentFormat = depthAttachmentFormat;
|
||||||
blend.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha;
|
this->config.stencilAttachmentFormat = stencilAttachmentFormat;
|
||||||
blend.srcAlphaBlendFactor = vk::BlendFactor::eOne;
|
|
||||||
blend.dstAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->config.depthStencilInfo.depthTestEnable = builder.depthTest;
|
|
||||||
this->config.depthStencilInfo.depthWriteEnable = builder.depthWrite;
|
|
||||||
this->config.depthStencilInfo.depthCompareOp = builder.depthCompareOp;
|
|
||||||
|
|
||||||
this->config.rasterizationInfo.cullMode = builder.cullMode;
|
|
||||||
this->config.rasterizationInfo.frontFace = builder.frontFace;
|
|
||||||
|
|
||||||
this->config.bindingDescriptions = builder.bindingDescriptions;
|
|
||||||
this->config.attributeDescriptions = builder.attributeDescriptions;
|
|
||||||
this->config.pipelineLayout = builder.pipelineLayout;
|
|
||||||
this->config.colorAttachmentFormats = builder.colorAttachmentFormats;
|
|
||||||
this->config.depthAttachmentFormat = builder.depthAttachmentFormat;
|
|
||||||
this->config.stencilAttachmentFormat = builder.stencilAttachmentFormat;
|
|
||||||
|
|
||||||
vk::UniqueShaderModule vertexShaderModule =
|
vk::UniqueShaderModule vertexShaderModule =
|
||||||
createShaderModule(builder.vertexShader->getSpirv());
|
createShaderModule(vertexShader.getSpirv());
|
||||||
vk::UniqueShaderModule fragmentShaderModule =
|
vk::UniqueShaderModule fragmentShaderModule =
|
||||||
createShaderModule(builder.fragmentShader->getSpirv());
|
createShaderModule(fragmentShader.getSpirv());
|
||||||
log::trace("created graphics shader modules");
|
log::trace("created graphics shader modules");
|
||||||
|
|
||||||
vk::PipelineShaderStageCreateInfo shaderStages[] = {
|
vk::PipelineShaderStageCreateInfo shaderStages[] = {
|
||||||
@@ -255,7 +227,7 @@ void GraphicsPipeline::setDefaultGraphicsPipelineConfig() {
|
|||||||
this->config.rasterizationInfo.polygonMode = vk::PolygonMode::eFill;
|
this->config.rasterizationInfo.polygonMode = vk::PolygonMode::eFill;
|
||||||
this->config.rasterizationInfo.lineWidth = 1.0f;
|
this->config.rasterizationInfo.lineWidth = 1.0f;
|
||||||
this->config.rasterizationInfo.cullMode = vk::CullModeFlagBits::eNone;
|
this->config.rasterizationInfo.cullMode = vk::CullModeFlagBits::eNone;
|
||||||
this->config.rasterizationInfo.frontFace = vk::FrontFace::eCounterClockwise;
|
this->config.rasterizationInfo.frontFace = vk::FrontFace::eClockwise;
|
||||||
this->config.rasterizationInfo.depthBiasEnable = vk::False;
|
this->config.rasterizationInfo.depthBiasEnable = vk::False;
|
||||||
this->config.rasterizationInfo.depthBiasConstantFactor = 0.0f;
|
this->config.rasterizationInfo.depthBiasConstantFactor = 0.0f;
|
||||||
this->config.rasterizationInfo.depthBiasClamp = 0.0f;
|
this->config.rasterizationInfo.depthBiasClamp = 0.0f;
|
||||||
+9
-21
@@ -6,9 +6,9 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
|
|
||||||
|
#include "device.hpp"
|
||||||
#include "pipeline.hpp"
|
#include "pipeline.hpp"
|
||||||
#include "vulkan/device.hpp"
|
#include "shader.hpp"
|
||||||
#include "vulkan/shader.hpp"
|
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
@@ -41,7 +41,13 @@ class GraphicsPipeline : public Pipeline {
|
|||||||
~GraphicsPipeline() override;
|
~GraphicsPipeline() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
explicit GraphicsPipeline(const Builder& builder);
|
GraphicsPipeline(
|
||||||
|
Device& device, Shader& vertexShader, Shader& fragmentShader,
|
||||||
|
vk::PipelineLayout pipelineLayout,
|
||||||
|
std::vector<vk::Format> colorAttachmentFormats,
|
||||||
|
vk::Format depthAttachmentFormat, vk::Format stencilAttachmentFormat,
|
||||||
|
std::vector<vk::VertexInputBindingDescription> bindingDescriptions,
|
||||||
|
std::vector<vk::VertexInputAttributeDescription> attributeDescriptions);
|
||||||
|
|
||||||
void setDefaultGraphicsPipelineConfig();
|
void setDefaultGraphicsPipelineConfig();
|
||||||
|
|
||||||
@@ -63,16 +69,6 @@ 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);
|
|
||||||
// Only matters with a depth attachment. Defaults to testing and writing
|
|
||||||
// with eLess.
|
|
||||||
Builder& setDepthTest(bool test, bool write,
|
|
||||||
vk::CompareOp compareOp = vk::CompareOp::eLess);
|
|
||||||
// Defaults to no culling, with counter-clockwise triangles facing front.
|
|
||||||
Builder& setCullMode(
|
|
||||||
vk::CullModeFlags cullMode,
|
|
||||||
vk::FrontFace frontFace = vk::FrontFace::eCounterClockwise);
|
|
||||||
|
|
||||||
std::unique_ptr<GraphicsPipeline> build() const;
|
std::unique_ptr<GraphicsPipeline> build() const;
|
||||||
|
|
||||||
@@ -87,14 +83,6 @@ 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;
|
|
||||||
bool depthTest = true;
|
|
||||||
bool depthWrite = true;
|
|
||||||
vk::CompareOp depthCompareOp = vk::CompareOp::eLess;
|
|
||||||
vk::CullModeFlags cullMode = vk::CullModeFlagBits::eNone;
|
|
||||||
vk::FrontFace frontFace = vk::FrontFace::eCounterClockwise;
|
|
||||||
|
|
||||||
friend class GraphicsPipeline;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace sophia
|
} // namespace sophia
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
|
|
||||||
#include "vulkan/device.hpp"
|
#include "device.hpp"
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
#include "renderer/render_context.hpp"
|
#include "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"
|
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
|
|
||||||
@@ -14,31 +11,22 @@ RenderContext::RenderContext(Window& window, Device& device)
|
|||||||
: window{window}, device{device} {
|
: window{window}, device{device} {
|
||||||
createFrames();
|
createFrames();
|
||||||
recreateSwapchain();
|
recreateSwapchain();
|
||||||
createTracyContext();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderContext::~RenderContext() {
|
RenderContext::~RenderContext() { this->device.waitIdle(); }
|
||||||
this->device.waitIdle();
|
|
||||||
TracyVkDestroy(this->tracyContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
vk::Result fenceResult = this->device.get()->waitForFences(
|
||||||
{
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
vk::Semaphore imageAvailable = frame.imageAvailable.get();
|
vk::Semaphore imageAvailable = frame.imageAvailable.get();
|
||||||
@@ -62,7 +50,7 @@ vk::CommandBuffer RenderContext::beginFrame() {
|
|||||||
imageInFlight, vk::True, std::numeric_limits<u64>::max());
|
imageInFlight, 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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,42 +67,33 @@ vk::CommandBuffer RenderContext::beginFrame() {
|
|||||||
throw std::runtime_error("failed to begin recording command buffer");
|
throw std::runtime_error("failed to begin recording command buffer");
|
||||||
}
|
}
|
||||||
|
|
||||||
TracyVkCollect(this->tracyContext,
|
|
||||||
static_cast<VkCommandBuffer>(frame.commandBuffer));
|
|
||||||
|
|
||||||
return frame.commandBuffer;
|
return frame.commandBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 =
|
||||||
|
vk::PipelineStageFlagBits::eColorAttachmentOutput;
|
||||||
|
vk::CommandBuffer commandBuffer = frame.commandBuffer;
|
||||||
|
|
||||||
vk::SemaphoreSubmitInfo waitInfo{};
|
vk::SubmitInfo submitInfo{};
|
||||||
waitInfo.semaphore = frame.imageAvailable.get();
|
submitInfo.waitSemaphoreCount = 1;
|
||||||
waitInfo.stageMask = SWAPCHAIN_ACQUIRE_STAGE;
|
submitInfo.pWaitSemaphores = &waitSemaphore;
|
||||||
|
submitInfo.pWaitDstStageMask = &waitStage;
|
||||||
|
submitInfo.commandBufferCount = 1;
|
||||||
|
submitInfo.pCommandBuffers = &commandBuffer;
|
||||||
|
submitInfo.signalSemaphoreCount = 1;
|
||||||
|
submitInfo.pSignalSemaphores = &signalSemaphore;
|
||||||
|
|
||||||
// Present has no pipeline stage to wait on, so signal once everything in
|
this->device.getGraphicsQueue().submit(submitInfo, frame.inFlight.get());
|
||||||
// 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);
|
||||||
@@ -165,27 +144,6 @@ void RenderContext::recreateSwapchain() {
|
|||||||
this->imagesInFlight.assign(this->swapchain->imageCount(), vk::Fence{});
|
this->imagesInFlight.assign(this->swapchain->imageCount(), vk::Fence{});
|
||||||
}
|
}
|
||||||
|
|
||||||
void RenderContext::createTracyContext() {
|
|
||||||
#ifdef TRACY_ENABLE
|
|
||||||
// Tracy records and submits a one-off command buffer to line up GPU and CPU
|
|
||||||
// clocks, and waits for it.
|
|
||||||
vk::CommandBufferAllocateInfo allocInfo{this->device.getCommandPool(),
|
|
||||||
vk::CommandBufferLevel::ePrimary, 1};
|
|
||||||
vk::CommandBuffer commandBuffer =
|
|
||||||
this->device.get()->allocateCommandBuffers(allocInfo).front();
|
|
||||||
|
|
||||||
this->tracyContext = TracyVkContext(
|
|
||||||
static_cast<VkPhysicalDevice>(this->device.getPhysicalDevice()),
|
|
||||||
static_cast<VkDevice>(*this->device.get()),
|
|
||||||
static_cast<VkQueue>(this->device.getGraphicsQueue()),
|
|
||||||
static_cast<VkCommandBuffer>(commandBuffer));
|
|
||||||
TracyVkContextName(this->tracyContext, "Graphics", 8);
|
|
||||||
|
|
||||||
this->device.get()->freeCommandBuffers(this->device.getCommandPool(),
|
|
||||||
commandBuffer);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderContext::createFrames() {
|
void RenderContext::createFrames() {
|
||||||
vk::CommandBufferAllocateInfo allocInfo{this->device.getCommandPool(),
|
vk::CommandBufferAllocateInfo allocInfo{this->device.getCommandPool(),
|
||||||
vk::CommandBufferLevel::ePrimary,
|
vk::CommandBufferLevel::ePrimary,
|
||||||
@@ -6,11 +6,8 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <vulkan/vulkan.hpp>
|
#include <vulkan/vulkan.hpp>
|
||||||
|
|
||||||
/* AFTER VULKAN */
|
#include "device.hpp"
|
||||||
#include <tracy/TracyVulkan.hpp>
|
#include "swapchain.hpp"
|
||||||
|
|
||||||
#include "vulkan/device.hpp"
|
|
||||||
#include "vulkan/swapchain.hpp"
|
|
||||||
#include "window.hpp"
|
#include "window.hpp"
|
||||||
|
|
||||||
namespace sophia {
|
namespace sophia {
|
||||||
@@ -34,12 +31,13 @@ class RenderContext {
|
|||||||
vk::CommandBuffer beginFrame();
|
vk::CommandBuffer beginFrame();
|
||||||
void endFrame();
|
void endFrame();
|
||||||
|
|
||||||
Image& getCurrentImage() const {
|
void transitionCurrentImage(vk::CommandBuffer commandBuffer,
|
||||||
return this->swapchain->getImage(static_cast<int>(this->currentImageIndex));
|
vk::ImageLayout newLayout) const {
|
||||||
|
this->swapchain->getImage(static_cast<int>(this->currentImageIndex))
|
||||||
|
.transitionLayout(commandBuffer, newLayout);
|
||||||
}
|
}
|
||||||
|
vk::ImageView getCurrentImageView() const {
|
||||||
Image& getCurrentDepthImage() const {
|
return this->swapchain->getImageView(
|
||||||
return this->swapchain->getDepthImage(
|
|
||||||
static_cast<int>(this->currentImageIndex));
|
static_cast<int>(this->currentImageIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,8 +45,6 @@ class RenderContext {
|
|||||||
vk::Format getImageFormat() const {
|
vk::Format getImageFormat() const {
|
||||||
return this->swapchain->getImageFormat();
|
return this->swapchain->getImageFormat();
|
||||||
}
|
}
|
||||||
u32 getFrameIndex() const { return this->currentFrameIndex; }
|
|
||||||
TracyVkCtx getTracyContext() const { return this->tracyContext; }
|
|
||||||
u32 getImageCount() const {
|
u32 getImageCount() const {
|
||||||
return static_cast<u32>(this->swapchain->imageCount());
|
return static_cast<u32>(this->swapchain->imageCount());
|
||||||
}
|
}
|
||||||
@@ -56,7 +52,6 @@ class RenderContext {
|
|||||||
private:
|
private:
|
||||||
void recreateSwapchain();
|
void recreateSwapchain();
|
||||||
void createFrames();
|
void createFrames();
|
||||||
void createTracyContext();
|
|
||||||
|
|
||||||
Window& window;
|
Window& window;
|
||||||
Device& device;
|
Device& device;
|
||||||
@@ -68,8 +63,6 @@ class RenderContext {
|
|||||||
std::vector<vk::UniqueSemaphore> renderFinishedSemaphores;
|
std::vector<vk::UniqueSemaphore> renderFinishedSemaphores;
|
||||||
std::vector<vk::Fence> imagesInFlight;
|
std::vector<vk::Fence> imagesInFlight;
|
||||||
|
|
||||||
TracyVkCtx tracyContext = nullptr;
|
|
||||||
|
|
||||||
u32 currentFrameIndex = 0;
|
u32 currentFrameIndex = 0;
|
||||||
u32 currentImageIndex = 0;
|
u32 currentImageIndex = 0;
|
||||||
bool isFrameStarted = false;
|
bool isFrameStarted = false;
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
#include "renderer/imgui_renderer.hpp"
|
|
||||||
|
|
||||||
#include <imgui.h>
|
|
||||||
#include <imgui_impl_glfw.h>
|
|
||||||
#include <imgui_impl_vulkan.h>
|
|
||||||
#include <imgui_internal.h>
|
|
||||||
|
|
||||||
#include <tracy/Tracy.hpp>
|
|
||||||
|
|
||||||
#include "resources/render_target.hpp"
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
constexpr ImGuiDockNodeFlags DOCKSPACE_FLAGS =
|
|
||||||
ImGuiDockNodeFlags_PassthruCentralNode;
|
|
||||||
|
|
||||||
void dockAll(const std::vector<std::string>& windows, ImGuiID nodeId) {
|
|
||||||
for (const std::string& name : windows) {
|
|
||||||
ImGui::DockBuilderDockWindow(name.c_str(), nodeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
ImGuiRenderer::ImGuiRenderer(Window& window, Device& device,
|
|
||||||
vk::Format colorAttachmentFormat,
|
|
||||||
u32 minImageCount, ResourceRegistry& resources)
|
|
||||||
: resources{resources} {
|
|
||||||
// 1. Construct ImGui context
|
|
||||||
IMGUI_CHECKVERSION();
|
|
||||||
ImGui::CreateContext();
|
|
||||||
|
|
||||||
ImGui_ImplGlfw_InitForVulkan(window.getGLFWwindow(), true);
|
|
||||||
|
|
||||||
ImGui_ImplVulkan_InitInfo initInfo = {};
|
|
||||||
device.populateImGuiInitInfo(initInfo);
|
|
||||||
initInfo.DescriptorPoolSize =
|
|
||||||
IMGUI_IMPL_VULKAN_MINIMUM_SAMPLED_IMAGE_POOL_SIZE + 5;
|
|
||||||
initInfo.MinImageCount = minImageCount;
|
|
||||||
initInfo.ImageCount = minImageCount;
|
|
||||||
|
|
||||||
VkFormat rawColorAttachmentFormat =
|
|
||||||
static_cast<VkFormat>(colorAttachmentFormat);
|
|
||||||
initInfo.UseDynamicRendering = true;
|
|
||||||
initInfo.PipelineInfoMain.PipelineRenderingCreateInfo.sType =
|
|
||||||
VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR;
|
|
||||||
initInfo.PipelineInfoMain.PipelineRenderingCreateInfo.colorAttachmentCount =
|
|
||||||
1;
|
|
||||||
initInfo.PipelineInfoMain.PipelineRenderingCreateInfo
|
|
||||||
.pColorAttachmentFormats = &rawColorAttachmentFormat;
|
|
||||||
|
|
||||||
ImGui_ImplVulkan_Init(&initInfo);
|
|
||||||
|
|
||||||
// 2. Enable dark theme
|
|
||||||
ImGui::StyleColorsDark();
|
|
||||||
|
|
||||||
// 3. Enable docking / keyboard
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
|
||||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
|
||||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When viewports are enabled we tweak WindowRounding/WindowBg so
|
|
||||||
* platform windows can look identical to regular ones.
|
|
||||||
*/
|
|
||||||
ImGuiStyle& style = ImGui::GetStyle();
|
|
||||||
if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
|
|
||||||
style.WindowRounding = 0.0f;
|
|
||||||
style.Colors[ImGuiCol_WindowBg].w = 1.0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGuiRenderer::~ImGuiRenderer() {
|
|
||||||
for (const auto& [index, texture] : textures) {
|
|
||||||
ImGui_ImplVulkan_RemoveTexture(texture.descriptorSet);
|
|
||||||
}
|
|
||||||
ImGui_ImplVulkan_Shutdown();
|
|
||||||
ImGui_ImplGlfw_Shutdown();
|
|
||||||
ImGui::DestroyContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImGuiRenderer::beginFrame() {
|
|
||||||
ZoneScoped;
|
|
||||||
releaseStaleTextures();
|
|
||||||
|
|
||||||
ImGui_ImplVulkan_NewFrame();
|
|
||||||
ImGui_ImplGlfw_NewFrame();
|
|
||||||
ImGui::NewFrame();
|
|
||||||
|
|
||||||
ImGuiID dockspaceId = ImGui::DockSpaceOverViewport(
|
|
||||||
0, ImGui::GetMainViewport(), DOCKSPACE_FLAGS);
|
|
||||||
applyLayoutIfNeeded(dockspaceId, DOCKSPACE_FLAGS);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImTextureID ImGuiRenderer::getTextureId(RenderTargetHandle handle) {
|
|
||||||
const RenderTarget& target = resources.renderTargetPool.get(handle);
|
|
||||||
vk::ImageView view = target.getImage().getView();
|
|
||||||
|
|
||||||
auto it = textures.find(handle.index);
|
|
||||||
if (it != textures.end()) {
|
|
||||||
if (it->second.generation == handle.generation &&
|
|
||||||
it->second.version == target.getVersion()) {
|
|
||||||
return (ImTextureID)it->second.descriptorSet;
|
|
||||||
}
|
|
||||||
ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet);
|
|
||||||
textures.erase(it);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImGui owns the sampler; it only needs the view and the layout the image is
|
|
||||||
// left in once the scene pass has finished.
|
|
||||||
VkDescriptorSet descriptorSet = ImGui_ImplVulkan_AddTexture(
|
|
||||||
static_cast<VkImageView>(view), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
|
||||||
textures[handle.index] = {handle.generation, target.getVersion(),
|
|
||||||
descriptorSet};
|
|
||||||
return (ImTextureID)descriptorSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resizing and releasing a target both wait for the GPU before destroying its
|
|
||||||
// image, so nothing in flight can still be using a stale texture.
|
|
||||||
void ImGuiRenderer::releaseStaleTextures() {
|
|
||||||
for (auto it = textures.begin(); it != textures.end();) {
|
|
||||||
RenderTargetHandle handle{it->first, it->second.generation};
|
|
||||||
RenderTargetPool& renderTargetPool = resources.renderTargetPool;
|
|
||||||
|
|
||||||
bool stale =
|
|
||||||
!renderTargetPool.isAlive(handle) ||
|
|
||||||
renderTargetPool.get(handle).getVersion() != it->second.version;
|
|
||||||
if (stale) {
|
|
||||||
ImGui_ImplVulkan_RemoveTexture(it->second.descriptorSet);
|
|
||||||
it = textures.erase(it);
|
|
||||||
} else {
|
|
||||||
++it;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImGuiRenderer::setDefaultLayout(DockLayout layout) {
|
|
||||||
defaultLayout = std::move(layout);
|
|
||||||
seedIfUnsaved = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImGuiRenderer::resetLayout() { forceReset = true; }
|
|
||||||
|
|
||||||
void ImGuiRenderer::applyLayoutIfNeeded(ImGuiID dockspaceId,
|
|
||||||
ImGuiDockNodeFlags flags) {
|
|
||||||
if (defaultLayout.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool apply = forceReset;
|
|
||||||
if (seedIfUnsaved) {
|
|
||||||
// A layout restored from imgui.ini shows up as a split root node.
|
|
||||||
ImGuiDockNode* root = ImGui::DockBuilderGetNode(dockspaceId);
|
|
||||||
apply = apply || root == nullptr || !root->IsSplitNode();
|
|
||||||
seedIfUnsaved = false;
|
|
||||||
}
|
|
||||||
if (!apply) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
forceReset = false;
|
|
||||||
|
|
||||||
const ImVec2 size = ImGui::GetMainViewport()->Size;
|
|
||||||
|
|
||||||
ImGui::DockBuilderRemoveNode(dockspaceId);
|
|
||||||
ImGui::DockBuilderAddNode(dockspaceId, flags | ImGuiDockNodeFlags_DockSpace);
|
|
||||||
ImGui::DockBuilderSetNodeSize(dockspaceId, size);
|
|
||||||
|
|
||||||
ImGuiID center = dockspaceId;
|
|
||||||
const ImGuiID left = ImGui::DockBuilderSplitNode(
|
|
||||||
center, ImGuiDir_Left, defaultLayout.leftWidth, nullptr, ¢er);
|
|
||||||
// The right split is taken from what's left after the left split.
|
|
||||||
const float rightRatio =
|
|
||||||
defaultLayout.rightWidth / (1.0f - defaultLayout.leftWidth);
|
|
||||||
const ImGuiID right = ImGui::DockBuilderSplitNode(
|
|
||||||
center, ImGuiDir_Right, rightRatio, nullptr, ¢er);
|
|
||||||
const ImGuiID bottom = ImGui::DockBuilderSplitNode(
|
|
||||||
center, ImGuiDir_Down, defaultLayout.bottomHeight, nullptr, ¢er);
|
|
||||||
|
|
||||||
dockAll(defaultLayout.left, left);
|
|
||||||
dockAll(defaultLayout.right, right);
|
|
||||||
dockAll(defaultLayout.bottom, bottom);
|
|
||||||
dockAll(defaultLayout.center, center);
|
|
||||||
|
|
||||||
ImGui::DockBuilderFinish(dockspaceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImGuiRenderer::endFrame(vk::CommandBuffer commandBuffer) {
|
|
||||||
ZoneScoped;
|
|
||||||
ImGui::Render();
|
|
||||||
ImDrawData* mainDrawData = ImGui::GetDrawData();
|
|
||||||
|
|
||||||
ImGuiIO& io = ImGui::GetIO();
|
|
||||||
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
|
|
||||||
ImGui::UpdatePlatformWindows();
|
|
||||||
ImGui::RenderPlatformWindowsDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui_ImplVulkan_RenderDrawData(mainDrawData, commandBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <imgui.h>
|
|
||||||
|
|
||||||
#include <sophia/handles.hpp>
|
|
||||||
#include <sophia/types.hpp>
|
|
||||||
#include <sophia/ui/dock_layout.hpp>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
#include "resources/resource_registry.hpp"
|
|
||||||
#include "vulkan/device.hpp"
|
|
||||||
#include "window.hpp"
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
class ImGuiRenderer {
|
|
||||||
public:
|
|
||||||
ImGuiRenderer(const ImGuiRenderer&) = delete;
|
|
||||||
ImGuiRenderer& operator=(const ImGuiRenderer&) = delete;
|
|
||||||
|
|
||||||
ImGuiRenderer(Window& window, Device& device,
|
|
||||||
vk::Format colorAttachmentFormat, u32 minImageCount,
|
|
||||||
ResourceRegistry& resources);
|
|
||||||
~ImGuiRenderer();
|
|
||||||
|
|
||||||
void beginFrame();
|
|
||||||
void endFrame(vk::CommandBuffer commandBuffer);
|
|
||||||
|
|
||||||
ImTextureID getTextureId(RenderTargetHandle handle);
|
|
||||||
|
|
||||||
void setDefaultLayout(DockLayout layout);
|
|
||||||
void resetLayout();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void applyLayoutIfNeeded(ImGuiID dockspaceId, ImGuiDockNodeFlags flags);
|
|
||||||
void releaseStaleTextures();
|
|
||||||
|
|
||||||
struct Texture {
|
|
||||||
u32 generation;
|
|
||||||
u32 version;
|
|
||||||
VkDescriptorSet descriptorSet;
|
|
||||||
};
|
|
||||||
|
|
||||||
ResourceRegistry& resources;
|
|
||||||
|
|
||||||
// Keyed by RenderTargetHandle::index.
|
|
||||||
std::unordered_map<u32, Texture> textures;
|
|
||||||
|
|
||||||
DockLayout defaultLayout;
|
|
||||||
bool seedIfUnsaved = false;
|
|
||||||
bool forceReset = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
#include "renderer/mesh_render_system.hpp"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <sophia/util/logger.hpp>
|
|
||||||
#include <tracy/Tracy.hpp>
|
|
||||||
|
|
||||||
#include "shaders/mesh.frag.spv.h"
|
|
||||||
#include "shaders/mesh.vert.spv.h"
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
// Mesh::getAttributeDescriptions() takes locations below this.
|
|
||||||
constexpr u32 FIRST_INSTANCE_LOCATION = 4;
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
MeshRenderSystem::MeshRenderSystem(Device& device, vk::Format colorFormat,
|
|
||||||
vk::Format depthFormat,
|
|
||||||
ResourceRegistry& resources)
|
|
||||||
: device{device}, resources{resources} {
|
|
||||||
createPipelineLayout();
|
|
||||||
createPipeline(colorFormat, depthFormat);
|
|
||||||
createInstanceBuffers();
|
|
||||||
}
|
|
||||||
|
|
||||||
MeshRenderSystem::~MeshRenderSystem() {
|
|
||||||
log::trace("destroyed sophia::MeshRenderSystem");
|
|
||||||
}
|
|
||||||
|
|
||||||
void MeshRenderSystem::beginFrame(u32 frameIndex) {
|
|
||||||
this->frameIndex = frameIndex;
|
|
||||||
this->instanceCursor = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MeshRenderSystem::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(
|
|
||||||
"MeshRenderSystem: frame instance buffer full, dropping {} meshes",
|
|
||||||
instances.size() - available);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Binding 1 stays bound across batches; each mesh rebinds binding 0.
|
|
||||||
vk::Buffer instanceBuffer = buffer.getBuffer();
|
|
||||||
vk::DeviceSize offset = 0;
|
|
||||||
commandBuffer.bindVertexBuffers(1, 1, &instanceBuffer, &offset);
|
|
||||||
|
|
||||||
for (const Batch& batch : batches) {
|
|
||||||
if (batch.firstInstance >= count) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
u32 batchCount = std::min(batch.instanceCount, count - batch.firstInstance);
|
|
||||||
|
|
||||||
const Mesh& mesh = this->resources.meshPool.get(batch.mesh);
|
|
||||||
mesh.bind(commandBuffer);
|
|
||||||
mesh.draw(commandBuffer, batchCount, base + batch.firstInstance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void MeshRenderSystem::createPipelineLayout() {
|
|
||||||
vk::PushConstantRange pushConstantRange{};
|
|
||||||
pushConstantRange.stageFlags = vk::ShaderStageFlagBits::eVertex;
|
|
||||||
pushConstantRange.offset = 0;
|
|
||||||
pushConstantRange.size = sizeof(PushConstantData);
|
|
||||||
|
|
||||||
vk::PipelineLayoutCreateInfo pipelineLayoutInfo = {};
|
|
||||||
pipelineLayoutInfo.setLayoutCount = 0;
|
|
||||||
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 MeshRenderSystem::createPipeline(vk::Format colorFormat,
|
|
||||||
vk::Format depthFormat) {
|
|
||||||
auto vertexShader = Shader::Builder()
|
|
||||||
.fromSPIRV(shaders::mesh_vert_spv)
|
|
||||||
.setStage(ShaderStage::VERTEX)
|
|
||||||
.build();
|
|
||||||
auto fragmentShader = Shader::Builder()
|
|
||||||
.fromSPIRV(shaders::mesh_frag_spv)
|
|
||||||
.setStage(ShaderStage::FRAGMENT)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
std::vector<vk::VertexInputBindingDescription> bindings =
|
|
||||||
Mesh::getBindingDescriptions();
|
|
||||||
bindings.push_back({1, sizeof(Instance), vk::VertexInputRate::eInstance});
|
|
||||||
|
|
||||||
std::vector<vk::VertexInputAttributeDescription> attributes =
|
|
||||||
Mesh::getAttributeDescriptions();
|
|
||||||
// A mat4 attribute takes four consecutive locations, one per column.
|
|
||||||
for (u32 column = 0; column < 4; column++) {
|
|
||||||
attributes.push_back({FIRST_INSTANCE_LOCATION + column, 1,
|
|
||||||
vk::Format::eR32G32B32A32Sfloat,
|
|
||||||
static_cast<u32>(offsetof(Instance, model) +
|
|
||||||
column * sizeof(glm::vec4))});
|
|
||||||
}
|
|
||||||
attributes.push_back({FIRST_INSTANCE_LOCATION + 4, 1,
|
|
||||||
vk::Format::eR32G32B32A32Sfloat,
|
|
||||||
static_cast<u32>(offsetof(Instance, color))});
|
|
||||||
|
|
||||||
this->graphicsPipeline = GraphicsPipeline::Builder(this->device)
|
|
||||||
.setVertexShader(*vertexShader)
|
|
||||||
.setFragmentShader(*fragmentShader)
|
|
||||||
.setPipelineLayout(this->pipelineLayout.get())
|
|
||||||
.setColorAttachmentFormats({colorFormat})
|
|
||||||
.setDepthAttachmentFormat(depthFormat)
|
|
||||||
.setVertexInput(bindings, attributes)
|
|
||||||
.setDepthTest(true, true)
|
|
||||||
.setCullMode(vk::CullModeFlagBits::eBack)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MeshRenderSystem::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
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <memory>
|
|
||||||
#include <sophia/handles.hpp>
|
|
||||||
#include <sophia/math.hpp>
|
|
||||||
#include <span>
|
|
||||||
|
|
||||||
#include "renderer/render_context.hpp"
|
|
||||||
#include "resources/resource_registry.hpp"
|
|
||||||
#include "vulkan/buffer.hpp"
|
|
||||||
#include "vulkan/device.hpp"
|
|
||||||
#include "vulkan/pipelines/graphics_pipeline.hpp"
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws batches of opaque meshes, depth tested and back-face culled. Knows
|
|
||||||
* nothing about scenes: callers hand it instances grouped into runs that
|
|
||||||
* share a mesh.
|
|
||||||
*
|
|
||||||
* Unlit for now: each pixel is the vertex color times the instance color.
|
|
||||||
* Normals and UVs reach the vertex shader but aren't used yet.
|
|
||||||
*
|
|
||||||
* 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 MeshRenderSystem {
|
|
||||||
public:
|
|
||||||
// Layout must match the per-instance inputs in mesh.vert.
|
|
||||||
struct Instance {
|
|
||||||
glm::mat4 model{1.f};
|
|
||||||
glm::vec4 color{1.f};
|
|
||||||
};
|
|
||||||
|
|
||||||
// A run of consecutive instances of the same mesh.
|
|
||||||
struct Batch {
|
|
||||||
MeshHandle mesh;
|
|
||||||
u32 firstInstance;
|
|
||||||
u32 instanceCount;
|
|
||||||
};
|
|
||||||
|
|
||||||
static constexpr u32 MAX_INSTANCES_PER_FRAME = 32768;
|
|
||||||
|
|
||||||
MeshRenderSystem(const MeshRenderSystem&) = delete;
|
|
||||||
MeshRenderSystem& operator=(const MeshRenderSystem&) = delete;
|
|
||||||
|
|
||||||
MeshRenderSystem(Device& device, vk::Format colorFormat,
|
|
||||||
vk::Format depthFormat, ResourceRegistry& resources);
|
|
||||||
~MeshRenderSystem();
|
|
||||||
|
|
||||||
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};
|
|
||||||
};
|
|
||||||
|
|
||||||
void createPipelineLayout();
|
|
||||||
void createPipeline(vk::Format colorFormat, vk::Format depthFormat);
|
|
||||||
void createInstanceBuffers();
|
|
||||||
|
|
||||||
Device& device;
|
|
||||||
ResourceRegistry& resources;
|
|
||||||
|
|
||||||
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,195 +0,0 @@
|
|||||||
#include "renderer/render_graph.hpp"
|
|
||||||
|
|
||||||
#include <sophia/util/assert.hpp>
|
|
||||||
#include <tracy/Tracy.hpp>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include "vulkan/debug_label.hpp"
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
ImageRef RenderGraph::importImage(Image& image, Access finalAccess) {
|
|
||||||
ImageRef ref{static_cast<u32>(this->images.size())};
|
|
||||||
this->images.push_back({&image, finalAccess, image.getAccessState()});
|
|
||||||
return ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderGraph::addPass(std::string name, const SetupFn& setup,
|
|
||||||
ExecuteFn execute) {
|
|
||||||
Pass& pass = this->passes.emplace_back();
|
|
||||||
pass.name = std::move(name);
|
|
||||||
pass.execute = std::move(execute);
|
|
||||||
|
|
||||||
PassBuilder builder{*this, pass};
|
|
||||||
setup(builder);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderGraph::execute(vk::CommandBuffer commandBuffer) {
|
|
||||||
ZoneScoped;
|
|
||||||
for (Pass& pass : this->passes) {
|
|
||||||
this->recordPass(commandBuffer, pass);
|
|
||||||
}
|
|
||||||
|
|
||||||
PendingBarriers pending;
|
|
||||||
for (u32 i = 0; i < this->images.size(); i++) {
|
|
||||||
this->transitionTo(ImageRef{i}, getAccessState(this->images[i].finalAccess),
|
|
||||||
pending);
|
|
||||||
}
|
|
||||||
this->flush(commandBuffer, pending);
|
|
||||||
|
|
||||||
for (ImportedImage& imported : this->images) {
|
|
||||||
imported.image->recordLayout(imported.state.layout);
|
|
||||||
}
|
|
||||||
|
|
||||||
this->passes.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderGraph::transitionTo(ImageRef ref, const AccessState& next,
|
|
||||||
PendingBarriers& pending) {
|
|
||||||
SOPHIA_ASSERT(ref.isValid() && ref.index < this->images.size(),
|
|
||||||
"ImageRef doesn't belong to this graph");
|
|
||||||
|
|
||||||
ImportedImage& imported = this->images[ref.index];
|
|
||||||
|
|
||||||
if (needsBarrier(imported.state, next)) {
|
|
||||||
pending.barriers.push_back(makeImageBarrier(
|
|
||||||
imported.image->get(), imported.image->getSubresourceRange(),
|
|
||||||
imported.state, next));
|
|
||||||
imported.state = next;
|
|
||||||
} else {
|
|
||||||
// Same-layout read after read. Nothing to record, but a later write has
|
|
||||||
// to wait on this reader too.
|
|
||||||
imported.state.stage |= next.stage;
|
|
||||||
imported.state.access |= next.access;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderGraph::flush(vk::CommandBuffer commandBuffer,
|
|
||||||
PendingBarriers& pending) {
|
|
||||||
recordImageBarriers(commandBuffer, pending.barriers);
|
|
||||||
pending = PendingBarriers{};
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderGraph::recordPass(vk::CommandBuffer commandBuffer, Pass& pass) {
|
|
||||||
ZoneTransientN(cpuZone, pass.name.c_str(), true);
|
|
||||||
TracyVkZoneTransient(this->tracyContext, gpuZone,
|
|
||||||
static_cast<VkCommandBuffer>(commandBuffer),
|
|
||||||
pass.name.c_str(), this->tracyContext != nullptr);
|
|
||||||
beginDebugLabel(commandBuffer, pass.name.c_str());
|
|
||||||
|
|
||||||
PendingBarriers pending;
|
|
||||||
for (const ImageUse& use : pass.uses) {
|
|
||||||
this->transitionTo(use.image, getAccessState(use.access), pending);
|
|
||||||
}
|
|
||||||
this->flush(commandBuffer, pending);
|
|
||||||
|
|
||||||
if (pass.colorAttachments.empty() && !pass.depthAttachment) {
|
|
||||||
pass.execute(commandBuffer);
|
|
||||||
} else {
|
|
||||||
this->recordRendering(commandBuffer, pass);
|
|
||||||
}
|
|
||||||
|
|
||||||
endDebugLabel(commandBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RenderGraph::recordRendering(vk::CommandBuffer commandBuffer, Pass& pass) {
|
|
||||||
std::vector<vk::RenderingAttachmentInfo> attachments;
|
|
||||||
attachments.reserve(pass.colorAttachments.size());
|
|
||||||
|
|
||||||
ImageRef sizedBy = pass.colorAttachments.empty()
|
|
||||||
? pass.depthAttachment->image
|
|
||||||
: pass.colorAttachments.front().image;
|
|
||||||
vk::Extent3D imageExtent = this->images[sizedBy.index].image->getExtent();
|
|
||||||
vk::Extent2D extent{imageExtent.width, imageExtent.height};
|
|
||||||
|
|
||||||
for (const ColorAttachment& color : pass.colorAttachments) {
|
|
||||||
const Image& image = *this->images[color.image.index].image;
|
|
||||||
SOPHIA_ASSERT(image.getExtent().width == extent.width &&
|
|
||||||
image.getExtent().height == extent.height,
|
|
||||||
"Pass {} has color attachments of different sizes",
|
|
||||||
pass.name);
|
|
||||||
|
|
||||||
vk::RenderingAttachmentInfo attachment{};
|
|
||||||
attachment.imageView = image.getView();
|
|
||||||
attachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal;
|
|
||||||
attachment.loadOp = color.loadOp;
|
|
||||||
attachment.storeOp = vk::AttachmentStoreOp::eStore;
|
|
||||||
attachment.clearValue.color = vk::ClearColorValue{color.clearColor};
|
|
||||||
attachments.push_back(attachment);
|
|
||||||
}
|
|
||||||
|
|
||||||
vk::RenderingInfo renderingInfo{};
|
|
||||||
renderingInfo.renderArea = vk::Rect2D{{0, 0}, extent};
|
|
||||||
renderingInfo.layerCount = 1;
|
|
||||||
renderingInfo.colorAttachmentCount = static_cast<u32>(attachments.size());
|
|
||||||
renderingInfo.pColorAttachments = attachments.data();
|
|
||||||
|
|
||||||
vk::RenderingAttachmentInfo depthAttachment{};
|
|
||||||
if (pass.depthAttachment) {
|
|
||||||
const Image& image = *this->images[pass.depthAttachment->image.index].image;
|
|
||||||
SOPHIA_ASSERT(image.getExtent().width == extent.width &&
|
|
||||||
image.getExtent().height == extent.height,
|
|
||||||
"Pass {} has a depth attachment of a different size",
|
|
||||||
pass.name);
|
|
||||||
|
|
||||||
depthAttachment.imageView = image.getView();
|
|
||||||
depthAttachment.imageLayout =
|
|
||||||
vk::ImageLayout::eDepthStencilAttachmentOptimal;
|
|
||||||
depthAttachment.loadOp = pass.depthAttachment->loadOp;
|
|
||||||
depthAttachment.storeOp = vk::AttachmentStoreOp::eStore;
|
|
||||||
depthAttachment.clearValue.depthStencil =
|
|
||||||
vk::ClearDepthStencilValue{pass.depthAttachment->clearDepth, 0};
|
|
||||||
renderingInfo.pDepthAttachment = &depthAttachment;
|
|
||||||
}
|
|
||||||
|
|
||||||
commandBuffer.beginRendering(renderingInfo);
|
|
||||||
|
|
||||||
vk::Viewport viewport{0.0f,
|
|
||||||
0.0f,
|
|
||||||
static_cast<float>(extent.width),
|
|
||||||
static_cast<float>(extent.height),
|
|
||||||
0.0f,
|
|
||||||
1.0f};
|
|
||||||
vk::Rect2D scissor{{0, 0}, extent};
|
|
||||||
commandBuffer.setViewport(0, viewport);
|
|
||||||
commandBuffer.setScissor(0, scissor);
|
|
||||||
|
|
||||||
pass.execute(commandBuffer);
|
|
||||||
|
|
||||||
commandBuffer.endRendering();
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderGraph::PassBuilder& RenderGraph::PassBuilder::writeColor(
|
|
||||||
ImageRef image, vk::AttachmentLoadOp loadOp, ClearColor clearColor) {
|
|
||||||
this->use(image, Access::ColorWrite);
|
|
||||||
this->pass.colorAttachments.push_back({image, loadOp, clearColor});
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderGraph::PassBuilder& RenderGraph::PassBuilder::writeDepth(
|
|
||||||
ImageRef image, vk::AttachmentLoadOp loadOp, float clearDepth) {
|
|
||||||
SOPHIA_ASSERT(!this->pass.depthAttachment,
|
|
||||||
"Pass {} already has a depth attachment", this->pass.name);
|
|
||||||
this->use(image, Access::DepthWrite);
|
|
||||||
this->pass.depthAttachment = DepthAttachment{image, loadOp, clearDepth};
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderGraph::PassBuilder& RenderGraph::PassBuilder::sample(ImageRef image) {
|
|
||||||
return this->use(image, Access::SampledRead);
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderGraph::PassBuilder& RenderGraph::PassBuilder::use(ImageRef image,
|
|
||||||
Access access) {
|
|
||||||
SOPHIA_ASSERT(image.isValid() && image.index < this->graph.images.size(),
|
|
||||||
"ImageRef doesn't belong to this graph");
|
|
||||||
for (const ImageUse& existing : this->pass.uses) {
|
|
||||||
SOPHIA_ASSERT(existing.image.index != image.index,
|
|
||||||
"Pass {} uses the same image twice", this->pass.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
this->pass.uses.push_back({image, access});
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <functional>
|
|
||||||
#include <optional>
|
|
||||||
#include <sophia/types.hpp>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <vulkan/vulkan.hpp>
|
|
||||||
// After Vulkan, which it expects to already be declared.
|
|
||||||
#include <tracy/TracyVulkan.hpp>
|
|
||||||
|
|
||||||
#include "vulkan/access.hpp"
|
|
||||||
#include "vulkan/image.hpp"
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Names an image within one RenderGraph. Only valid for the graph that
|
|
||||||
* returned it.
|
|
||||||
*/
|
|
||||||
struct ImageRef {
|
|
||||||
u32 index = INVALID_INDEX;
|
|
||||||
|
|
||||||
static constexpr u32 INVALID_INDEX = 0xFFFFFFFF;
|
|
||||||
|
|
||||||
bool isValid() const { return this->index != INVALID_INDEX; }
|
|
||||||
};
|
|
||||||
|
|
||||||
using ClearColor = std::array<float, 4>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A frame's passes and the images they touch, recorded into a single command
|
|
||||||
* buffer. Passes run in the order they were added; the graph's job is to
|
|
||||||
* work out the barriers and layout transitions between them from what each
|
|
||||||
* pass says it does to each image, and to begin/end rendering for passes that
|
|
||||||
* write color or depth attachments.
|
|
||||||
*
|
|
||||||
* Built fresh every frame:
|
|
||||||
*
|
|
||||||
* RenderGraph graph;
|
|
||||||
* ImageRef backbuffer = graph.importImage(swapchainImage, Access::Present);
|
|
||||||
* graph.addPass(
|
|
||||||
* "main", [&](PassBuilder& pass) { pass.writeColor(backbuffer); },
|
|
||||||
* [&](vk::CommandBuffer cb) { draw(cb); });
|
|
||||||
* graph.execute(commandBuffer);
|
|
||||||
*
|
|
||||||
* Imported images start in whatever layout Image reports and are left in
|
|
||||||
* their final access when execute() returns, with the Image's tracked layout
|
|
||||||
* updated to match.
|
|
||||||
*/
|
|
||||||
class RenderGraph {
|
|
||||||
public:
|
|
||||||
class PassBuilder;
|
|
||||||
|
|
||||||
using SetupFn = std::function<void(PassBuilder&)>;
|
|
||||||
using ExecuteFn = std::function<void(vk::CommandBuffer)>;
|
|
||||||
|
|
||||||
explicit RenderGraph(TracyVkCtx tracyContext = nullptr)
|
|
||||||
: tracyContext{tracyContext} {}
|
|
||||||
|
|
||||||
RenderGraph(const RenderGraph&) = delete;
|
|
||||||
RenderGraph& operator=(const RenderGraph&) = delete;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Brings an image the graph doesn't own into it. It must outlive
|
|
||||||
* execute(). After the last pass it is transitioned to `finalAccess`.
|
|
||||||
*/
|
|
||||||
ImageRef importImage(Image& image, Access finalAccess);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a pass. `setup` runs immediately and declares what the pass does to
|
|
||||||
* images; `execute` runs later, from execute(), with the barriers already
|
|
||||||
* recorded and (if the pass writes color or depth) rendering begun. `name`
|
|
||||||
* labels the pass's commands in debugging tools such as RenderDoc.
|
|
||||||
*/
|
|
||||||
void addPass(std::string name, const SetupFn& setup, ExecuteFn execute);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Records every pass, and the barriers between them, into `commandBuffer`.
|
|
||||||
*/
|
|
||||||
void execute(vk::CommandBuffer commandBuffer);
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct ImageUse {
|
|
||||||
ImageRef image;
|
|
||||||
Access access;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ColorAttachment {
|
|
||||||
ImageRef image;
|
|
||||||
vk::AttachmentLoadOp loadOp;
|
|
||||||
ClearColor clearColor;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct DepthAttachment {
|
|
||||||
ImageRef image;
|
|
||||||
vk::AttachmentLoadOp loadOp;
|
|
||||||
float clearDepth;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Pass {
|
|
||||||
std::string name;
|
|
||||||
std::vector<ImageUse> uses;
|
|
||||||
std::vector<ColorAttachment> colorAttachments;
|
|
||||||
std::optional<DepthAttachment> depthAttachment;
|
|
||||||
ExecuteFn execute;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ImportedImage {
|
|
||||||
Image* image;
|
|
||||||
Access finalAccess;
|
|
||||||
// What the image was last left in, as of the barriers recorded so far.
|
|
||||||
AccessState state;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PendingBarriers {
|
|
||||||
std::vector<vk::ImageMemoryBarrier2> barriers;
|
|
||||||
};
|
|
||||||
|
|
||||||
void transitionTo(ImageRef ref, const AccessState& next,
|
|
||||||
PendingBarriers& pending);
|
|
||||||
void flush(vk::CommandBuffer commandBuffer, PendingBarriers& pending);
|
|
||||||
void recordPass(vk::CommandBuffer commandBuffer, Pass& pass);
|
|
||||||
void recordRendering(vk::CommandBuffer commandBuffer, Pass& pass);
|
|
||||||
|
|
||||||
TracyVkCtx tracyContext;
|
|
||||||
std::vector<ImportedImage> images;
|
|
||||||
std::vector<Pass> passes;
|
|
||||||
};
|
|
||||||
|
|
||||||
class RenderGraph::PassBuilder {
|
|
||||||
public:
|
|
||||||
/**
|
|
||||||
* Renders into `image` as a color attachment. Passes with one or more of
|
|
||||||
* these get rendering begun (and viewport/scissor set to the attachment's
|
|
||||||
* extent) before their execute callback runs.
|
|
||||||
*/
|
|
||||||
PassBuilder& writeColor(
|
|
||||||
ImageRef image,
|
|
||||||
vk::AttachmentLoadOp loadOp = vk::AttachmentLoadOp::eClear,
|
|
||||||
ClearColor clearColor = {0.f, 0.f, 0.f, 1.f});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders with `image` as the depth attachment, which must be the same size
|
|
||||||
* as any color attachments. At most one per pass.
|
|
||||||
*/
|
|
||||||
PassBuilder& writeDepth(
|
|
||||||
ImageRef image,
|
|
||||||
vk::AttachmentLoadOp loadOp = vk::AttachmentLoadOp::eClear,
|
|
||||||
float clearDepth = 1.f);
|
|
||||||
|
|
||||||
/** Samples `image` in a shader. */
|
|
||||||
PassBuilder& sample(ImageRef image);
|
|
||||||
|
|
||||||
/** Any other use, e.g. storage or transfer access. */
|
|
||||||
PassBuilder& use(ImageRef image, Access access);
|
|
||||||
|
|
||||||
private:
|
|
||||||
friend class RenderGraph;
|
|
||||||
|
|
||||||
PassBuilder(RenderGraph& graph, Pass& pass) : graph{graph}, pass{pass} {}
|
|
||||||
|
|
||||||
RenderGraph& graph;
|
|
||||||
Pass& pass;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
#include "renderer/renderer.hpp"
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
Renderer::Renderer() {}
|
|
||||||
|
|
||||||
Renderer::~Renderer() {}
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <sophia/handles.hpp>
|
|
||||||
#include <sophia/math.hpp>
|
|
||||||
#include <sophia/types.hpp>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "vulkan/buffer.hpp"
|
|
||||||
|
|
||||||
namespace sophia {
|
|
||||||
|
|
||||||
struct DrawPacket {
|
|
||||||
/* Geometry */
|
|
||||||
// BufferHandle vertexBuffer;
|
|
||||||
// BufferHandle indexBuffer;
|
|
||||||
u32 indexCount;
|
|
||||||
u32 firstIndex;
|
|
||||||
|
|
||||||
/* Shader & state */
|
|
||||||
// PipelineHandle pipelineState; // Contains shaders, blend state, depth
|
|
||||||
// settings
|
|
||||||
|
|
||||||
/* Bindings */
|
|
||||||
// DescriptorSetHandle resourceBindings; // Textures, Uniform Buffers,
|
|
||||||
// Storage Buffers
|
|
||||||
|
|
||||||
/* Transform / Constants */
|
|
||||||
u32 pushConstantData[16];
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RenderView {
|
|
||||||
mat4 view;
|
|
||||||
mat4 projection;
|
|
||||||
vec2 viewport;
|
|
||||||
|
|
||||||
RenderTargetHandle target;
|
|
||||||
};
|
|
||||||
|
|
||||||
// struct ComputePacket {
|
|
||||||
// ComputePipelineHandle pipeline;
|
|
||||||
// DescriptorSetHandle
|
|
||||||
// resourceBindings; // Includes storage buffers, uniforms, textures
|
|
||||||
|
|
||||||
// // Grid dimensions
|
|
||||||
// uint32_t groupCountX = 1;
|
|
||||||
// uint32_t groupCountY = 1;
|
|
||||||
// uint32_t groupCountZ = 1;
|
|
||||||
|
|
||||||
// // Optional push constants for dynamic per-dispatch parameters
|
|
||||||
// uint8_t pushConstants[64];
|
|
||||||
// uint32_t pushConstantSize = 0;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// !TODO move to render graph
|
|
||||||
enum class RenderPassType { Graphics, Compute };
|
|
||||||
|
|
||||||
// !TODO move to render graph (???)
|
|
||||||
struct RenderPass {
|
|
||||||
std::string name;
|
|
||||||
RenderPassType type;
|
|
||||||
|
|
||||||
// Declared for automatic barrier injection & resource synchronization
|
|
||||||
std::vector<Buffer> readBuffers; // BufferHandle instead?
|
|
||||||
std::vector<Buffer> writeBuffers; // BufferHandle instead?
|
|
||||||
|
|
||||||
// Commands in this pass
|
|
||||||
// std::vector<ComputePacket> computeDispatches;
|
|
||||||
std::vector<DrawPacket> opaqueDrawPackets;
|
|
||||||
std::vector<DrawPacket> transparentDrawPackets;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RenderPacket {
|
|
||||||
RenderView view;
|
|
||||||
std::vector<RenderPass> passes;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Renderer {
|
|
||||||
public:
|
|
||||||
Renderer(const Renderer&) = delete;
|
|
||||||
Renderer& operator=(const Renderer&) = delete;
|
|
||||||
|
|
||||||
Renderer();
|
|
||||||
~Renderer();
|
|
||||||
|
|
||||||
// PipelineHandle createPipeline();
|
|
||||||
|
|
||||||
private:
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
#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},
|
|
||||||
meshRenderSystem{device, colorFormat,
|
|
||||||
resources.renderTargetPool.getDepthFormat(), resources},
|
|
||||||
spriteRenderSystem{device, colorFormat,
|
|
||||||
resources.renderTargetPool.getDepthFormat(),
|
|
||||||
resources} {}
|
|
||||||
|
|
||||||
void SceneRenderer::beginFrame(u32 frameIndex) {
|
|
||||||
ZoneScoped;
|
|
||||||
this->meshRenderSystem.beginFrame(frameIndex);
|
|
||||||
this->spriteRenderSystem.beginFrame(frameIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SceneRenderer::render(vk::CommandBuffer commandBuffer,
|
|
||||||
const SceneView& sceneView, Entity camera,
|
|
||||||
vk::Extent2D extent) {
|
|
||||||
ZoneScoped;
|
|
||||||
SOPHIA_ASSERT(camera.hasComponent<CameraComponent>(),
|
|
||||||
"camera does not have camera component!");
|
|
||||||
|
|
||||||
float aspect =
|
|
||||||
static_cast<float>(extent.width) / static_cast<float>(extent.height);
|
|
||||||
|
|
||||||
glm::mat4 view =
|
|
||||||
CameraComponent::getView(camera.getComponent<TransformComponent>());
|
|
||||||
glm::mat4 projection =
|
|
||||||
camera.getComponent<CameraComponent>().getProjection(aspect);
|
|
||||||
|
|
||||||
const Scene& scene = this->sceneRegistry.get(sceneView.scene);
|
|
||||||
glm::mat4 viewProjection = projection * view;
|
|
||||||
|
|
||||||
// Opaque first, so sprites blend over the meshes behind them.
|
|
||||||
renderMeshes(commandBuffer, scene, viewProjection);
|
|
||||||
renderSprites(commandBuffer, scene, view, viewProjection);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SceneRenderer::renderMeshes(vk::CommandBuffer commandBuffer,
|
|
||||||
const Scene& scene,
|
|
||||||
const glm::mat4& viewProjection) {
|
|
||||||
ZoneScoped;
|
|
||||||
this->meshDrawItems.clear();
|
|
||||||
|
|
||||||
{
|
|
||||||
ZoneScopedN("meshes: gather");
|
|
||||||
auto meshes =
|
|
||||||
scene.registry.view<const TransformComponent, const MeshComponent>();
|
|
||||||
for (auto [entity, transform, mesh] : meshes.each()) {
|
|
||||||
if (!this->resources.meshPool.isAlive(mesh.handle)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
this->meshDrawItems.push_back(
|
|
||||||
{mesh.handle, {transform.mat4(), mesh.color}});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
ZoneScopedN("meshes: sort");
|
|
||||||
// Opaque, so order only matters for batching: one batch per mesh.
|
|
||||||
std::ranges::sort(this->meshDrawItems, {},
|
|
||||||
[](const MeshDrawItem& item) { return item.mesh; });
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
ZoneScopedN("meshes: batch");
|
|
||||||
this->meshInstances.clear();
|
|
||||||
this->meshBatches.clear();
|
|
||||||
for (const MeshDrawItem& item : this->meshDrawItems) {
|
|
||||||
auto index = static_cast<u32>(this->meshInstances.size());
|
|
||||||
this->meshInstances.push_back(item.instance);
|
|
||||||
|
|
||||||
if (this->meshBatches.empty() ||
|
|
||||||
this->meshBatches.back().mesh != item.mesh) {
|
|
||||||
this->meshBatches.push_back({item.mesh, index, 0});
|
|
||||||
}
|
|
||||||
this->meshBatches.back().instanceCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TracyPlot("mesh instances", static_cast<int64_t>(this->meshInstances.size()));
|
|
||||||
TracyPlot("mesh batches", static_cast<int64_t>(this->meshBatches.size()));
|
|
||||||
|
|
||||||
this->meshRenderSystem.render(commandBuffer, viewProjection,
|
|
||||||
this->meshInstances, this->meshBatches);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SceneRenderer::renderSprites(vk::CommandBuffer commandBuffer,
|
|
||||||
const Scene& scene, const glm::mat4& view,
|
|
||||||
const glm::mat4& viewProjection) {
|
|
||||||
ZoneScoped;
|
|
||||||
this->spriteDrawItems.clear();
|
|
||||||
|
|
||||||
{
|
|
||||||
ZoneScopedN("sprites: gather");
|
|
||||||
auto sprites =
|
|
||||||
scene.registry.view<const TransformComponent, const SpriteComponent>();
|
|
||||||
for (auto [entity, transform, sprite] : sprites.each()) {
|
|
||||||
TextureHandle texture =
|
|
||||||
this->resources.texturePool.isAlive(sprite.texture)
|
|
||||||
? sprite.texture
|
|
||||||
: this->resources.whiteTexture;
|
|
||||||
|
|
||||||
// Distance in front of the camera, which looks down view-space -Z.
|
|
||||||
float depth = -(view * glm::vec4{transform.position, 1.f}).z;
|
|
||||||
|
|
||||||
this->spriteDrawItems.push_back(
|
|
||||||
{sprite.sortOrder,
|
|
||||||
depth,
|
|
||||||
texture,
|
|
||||||
{sprite.getModel(transform), sprite.color, sprite.uvRect}});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
ZoneScopedN("sprites: sort");
|
|
||||||
// Within a sortOrder, back to front, so nearer sprites blend over farther
|
|
||||||
// ones. 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};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
ZoneScopedN("sprites: batch");
|
|
||||||
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++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TracyPlot("sprite instances",
|
|
||||||
static_cast<int64_t>(this->spriteInstances.size()));
|
|
||||||
TracyPlot("sprite batches", static_cast<int64_t>(this->spriteBatches.size()));
|
|
||||||
|
|
||||||
this->spriteRenderSystem.render(commandBuffer, viewProjection,
|
|
||||||
this->spriteInstances, this->spriteBatches);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <sophia/scene_registry.hpp>
|
|
||||||
#include <sophia/scene_view.hpp>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "renderer/mesh_render_system.hpp"
|
|
||||||
#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, which is `extent` in size, must already have begun.
|
|
||||||
void render(vk::CommandBuffer commandBuffer, const SceneView& sceneView,
|
|
||||||
Entity camera, vk::Extent2D extent);
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct MeshDrawItem {
|
|
||||||
MeshHandle mesh;
|
|
||||||
MeshRenderSystem::Instance instance;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SpriteDrawItem {
|
|
||||||
s32 sortOrder;
|
|
||||||
// Distance in front of the camera.
|
|
||||||
float depth;
|
|
||||||
TextureHandle texture;
|
|
||||||
SpriteRenderSystem::Instance instance;
|
|
||||||
};
|
|
||||||
|
|
||||||
void renderMeshes(vk::CommandBuffer commandBuffer, const Scene& scene,
|
|
||||||
const glm::mat4& viewProjection);
|
|
||||||
void renderSprites(vk::CommandBuffer commandBuffer, const Scene& scene,
|
|
||||||
const glm::mat4& view, const glm::mat4& viewProjection);
|
|
||||||
|
|
||||||
const SceneRegistry& sceneRegistry;
|
|
||||||
ResourceRegistry& resources;
|
|
||||||
|
|
||||||
MeshRenderSystem meshRenderSystem;
|
|
||||||
SpriteRenderSystem spriteRenderSystem;
|
|
||||||
|
|
||||||
// Reused across frames to avoid reallocating.
|
|
||||||
std::vector<MeshDrawItem> meshDrawItems;
|
|
||||||
std::vector<MeshRenderSystem::Instance> meshInstances;
|
|
||||||
std::vector<MeshRenderSystem::Batch> meshBatches;
|
|
||||||
std::vector<SpriteDrawItem> spriteDrawItems;
|
|
||||||
std::vector<SpriteRenderSystem::Instance> spriteInstances;
|
|
||||||
std::vector<SpriteRenderSystem::Batch> spriteBatches;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace sophia
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
#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,
|
|
||||||
vk::Format depthFormat,
|
|
||||||
ResourceRegistry& resources)
|
|
||||||
: device{device}, resources{resources} {
|
|
||||||
createDescriptors();
|
|
||||||
createPipelineLayout();
|
|
||||||
createPipeline(colorFormat, depthFormat);
|
|
||||||
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 {} sprites",
|
|
||||||
instances.size() - available);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
vk::Format depthFormat) {
|
|
||||||
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})
|
|
||||||
.setDepthAttachmentFormat(depthFormat)
|
|
||||||
.setVertexInput(bindings, attributes)
|
|
||||||
.setAlphaBlending()
|
|
||||||
.setDepthTest(true, false, vk::CompareOp::eLessOrEqual)
|
|
||||||
.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
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user