Init squashed

This commit is contained in:
Lauri Räsänen 2024-09-17 19:14:58 +03:00
commit 4aac4b824f
95 changed files with 7582 additions and 0 deletions

205
.clang-format Normal file
View file

@ -0,0 +1,205 @@
---
BasedOnStyle: Microsoft
AccessModifierOffset: -2
AlignAfterOpenBracket: BlockIndent
AlignArrayOfStructures: Right
AlignConsecutiveAssignments:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: true
AlignConsecutiveBitFields:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignConsecutiveDeclarations:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignConsecutiveMacros:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: false
PadOperators: false
AlignConsecutiveShortCaseStatements:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignCaseColons: false
AlignEscapedNewlines: Left
AlignOperands: Align
AlignTrailingComments:
Kind: Always
OverEmptyLines: 0
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: Empty
AllowShortCaseLabelsOnASingleLine: false
AllowShortEnumsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: Never
AllowShortLambdasOnASingleLine: All
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: Yes
AttributeMacros:
- __capability
BinPackArguments: false
BinPackParameters: false
BitFieldColonSpacing: Both
BraceWrapping:
AfterCaseLabel: true
AfterClass: true
AfterControlStatement: Always
AfterEnum: true
AfterFunction: true
AfterNamespace: true
AfterObjCDeclaration: true
AfterStruct: true
AfterUnion: false
AfterExternBlock: true
BeforeCatch: true
BeforeElse: true
BeforeLambdaBody: false
BeforeWhile: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakAfterAttributes: Never
BreakAfterJavaFieldAnnotations: false
BreakArrays: true
BreakBeforeBinaryOperators: NonAssignment
BreakBeforeBraces: Custom
BreakBeforeConceptDeclarations: Always
BreakBeforeInlineASMColon: OnlyMultiline
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: AfterColon
BreakInheritanceList: AfterColon
BreakStringLiterals: true
ColumnLimit: 100
CommentPragmas: "^ IWYU pragma:"
CompactNamespaces: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
EmptyLineAfterAccessModifier: Never
EmptyLineBeforeAccessModifier: Always
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
IncludeBlocks: Preserve
IncludeIsMainRegex: (Test)?$
IncludeIsMainSourceRegex: ""
IndentAccessModifiers: false
IndentCaseBlocks: false
IndentCaseLabels: true
IndentExternBlock: AfterExternBlock
IndentGotoLabels: true
IndentPPDirectives: None
IndentRequiresClause: false
IndentWidth: 4
IndentWrappedFunctionNames: false
InsertBraces: true
InsertNewlineAtEOF: true
InsertTrailingCommas: Wrapped
IntegerLiteralSeparator:
Binary: 0
BinaryMinDigits: 0
Decimal: 0
DecimalMinDigits: 0
Hex: 0
HexMinDigits: 0
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtEOF: false
KeepEmptyLinesAtTheStartOfBlocks: true
LambdaBodyIndentation: Signature
Language: Cpp
LineEnding: LF
MacroBlockBegin: ""
MacroBlockEnd: ""
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBinPackProtocolList: Auto
ObjCBlockIndentWidth: 2
ObjCBreakBeforeNestedBlockParam: true
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PPIndentWidth: -1
PackConstructorInitializers: CurrentLine
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakOpenParenthesis: 0
PenaltyBreakString: 1000
PenaltyBreakTemplateDeclaration: 10
PenaltyExcessCharacter: 1000000
PenaltyIndentedWhitespace: 0
PenaltyReturnTypeOnItsOwnLine: 1000
PointerAlignment: Left
QualifierAlignment: Left
ReferenceAlignment: Pointer
ReflowComments: true
RemoveBracesLLVM: false
RemoveParentheses: Leave
RemoveSemicolon: false
RequiresClausePosition: OwnLine
RequiresExpressionIndentation: OuterScope
SeparateDefinitionBlocks: Always
ShortNamespaceLines: 1
SortIncludes: CaseInsensitive
SortJavaStaticImport: Before
SortUsingDeclarations: Lexicographic
SpaceAfterCStyleCast: false
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: false
SpaceAroundPointerQualifiers: Default
SpaceBeforeAssignmentOperators: true
SpaceBeforeCaseColon: false
SpaceBeforeCpp11BracedList: true
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeJsonColon: false
SpaceBeforeParens: ControlStatements
SpaceBeforeParensOptions:
AfterControlStatements: true
AfterForeachMacros: true
AfterFunctionDeclarationName: false
AfterFunctionDefinitionName: false
AfterIfMacros: true
AfterOverloadedOperator: false
AfterRequiresInClause: false
AfterRequiresInExpression: false
BeforeNonEmptyParentheses: false
SpaceBeforeRangeBasedForLoopColon: true
SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: Never
SpacesInContainerLiterals: false
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: -1
SpacesInParens: Never
SpacesInParensOptions:
InConditionalStatements: false
InCStyleCasts: false
InEmptyParentheses: false
Other: false
SpacesInSquareBrackets: false
Standard: Latest
TabWidth: 4
UseTab: Never
VerilogBreakBetweenInstancePorts: true

3
.clangd Normal file
View file

@ -0,0 +1,3 @@
CompileFlags:
CompilationDatabase: ./build/

0
.gitattributes vendored Normal file
View file

11
.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
subprojects/**/*
!subprojects/*.wrap
.cache/
lib/
include/steamworks
imgui.ini

15
.gitmodules vendored Normal file
View file

@ -0,0 +1,15 @@
[submodule "include/imgui"]
path = include/imgui
url = https://github.com/ocornut/imgui.git
[submodule "include/glm"]
path = include/glm
url = https://github.com/g-truc/glm.git
[submodule "include/VulkanMemoryAllocator"]
path = include/VulkanMemoryAllocator
url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
[submodule "include/PerlinNoise"]
path = include/PerlinNoise
url = https://github.com/Reputeless/PerlinNoise.git
[submodule "include/JoltPhysics"]
path = include/JoltPhysics
url = https://github.com/jrouwe/JoltPhysics.git

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 nullprop
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

30
README.md Normal file
View file

@ -0,0 +1,30 @@
# LEGS
Very WIP framework for 3D games.
**legs** help you run. (\*ba-dum-tss\*)
## Running
### Linux
Dependencies: Meson, gcc
```sh
git clone https://github.com/nullprop/legs.git --recursive
cd legs
./scripts/compile_jolt.sh distribution
./scripts/setup_release.sh
ninja -C build
./build/examples/02_systems/02_systems
```
## Third-party code
- [glm](https://github.com/g-truc/glm): MIT / The Happy Bunny License
- [imgui](https://github.com/ocornut/imgui): MIT License
- [JoltPhysics](https://github.com/jrouwe/JoltPhysics): MIT License
- [VulkanMemoryAllocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator): MIT License
- [SDL](https://github.com/libsdl-org/SDL) (system/wrapdb): Zlib License
- [Vulkan-Headers](https://github.com/KhronosGroup/Vulkan-Headers) (system/wrapdb): Apache-2.0 / MIT License

View file

@ -0,0 +1,19 @@
#include <legs/entry.hpp>
#include <legs/log.hpp>
using namespace legs;
int main(int argc, char** argv)
{
Log::SetLogLevel(LogLevel::Debug);
auto code = LEGS_Init(argc, argv);
if (code < 0)
{
return code;
}
g_engine->GetWindow()->SetTitle("01_hello_world");
return LEGS_Run();
}

View file

@ -0,0 +1 @@
executable('01_hello_world', files('main.cpp'), dependencies: [legs_dep])

View file

@ -0,0 +1,163 @@
#include <memory>
#include <legs/entry.hpp>
#include <legs/entity/mesh_entity.hpp>
#include <legs/entity/sky.hpp>
#include <legs/geometry/icosphere.hpp>
#include <legs/geometry/plane.hpp>
#include <legs/isystem.hpp>
#include <legs/log.hpp>
#include <legs/time.hpp>
using namespace legs;
class MySystem : public ISystem
{
public:
MySystem()
{
int width;
int height;
g_engine->GetWindow()->GetFramebufferSize(&width, &height);
m_camera = std::make_shared<NoclipCamera>(width, height);
m_camera->SetPosition({0.0f, -10.0f, 5.0f});
g_engine->SetCamera(m_camera);
auto renderer = g_engine->GetRenderer();
auto world = g_engine->GetWorld();
// Create a sky
auto sky = std::make_shared<Sky>(renderer);
world->SetSky(sky);
// Create some test spheres
for (unsigned int x = 0; x < 3; x++)
{
for (unsigned int y = 0; y < 3; y++)
{
auto testSphere = SIcosphere(glm::vec3(x, y, 5), 0.5f, 1);
std::shared_ptr<Buffer> sphereVertexBuffer;
std::shared_ptr<Buffer> sphereIndexBuffer;
std::vector<Vertex_P_N_C> sphereVertices;
sphereVertices.reserve(testSphere.positions.size());
for (unsigned int i = 0; i < testSphere.positions.size(); i++)
{
sphereVertices.push_back(
{testSphere.positions[i], testSphere.normals[i], glm::vec3(0.5, 0.5, 0.5)}
);
}
renderer->CreateBuffer(
sphereVertexBuffer,
VertexBuffer,
sphereVertices.data(),
sizeof(Vertex_P_N_C),
static_cast<uint32_t>(sphereVertices.size())
);
renderer->CreateBuffer(
sphereIndexBuffer,
IndexBuffer,
testSphere.indices.data(),
sizeof(Index),
static_cast<uint32_t>(testSphere.indices.size())
);
auto sphere = std::make_shared<MeshEntity>();
sphere->SetBuffers(sphereVertexBuffer, sphereIndexBuffer);
sphere->SetPipeline(RenderPipeline::GEO_P_N_C);
world->AddEntity(sphere);
m_spheres.push_back(sphere);
}
}
// Create a test plane
std::shared_ptr<Buffer> planeVertexBuffer;
std::shared_ptr<Buffer> planeIndexBuffer;
auto testPlane = SPlane({0.0f, 0.0f, 0.0f}, 20.0f);
auto planeVertices = std::array<Vertex_P_C, 4>();
for (unsigned int i = 0; i < 4; i++)
{
auto color = glm::vec3 {
i == 0 ? 1.0f : 0.0f,
i == 1 ? 1.0f : 0.0f,
i == 2 ? 1.0f : 0.0f,
};
planeVertices[i] = {testPlane.vertices[i], color};
}
renderer->CreateBuffer(
planeVertexBuffer,
VertexBuffer,
planeVertices.data(),
sizeof(Vertex_P_C),
static_cast<uint32_t>(planeVertices.size())
);
renderer->CreateBuffer(
planeIndexBuffer,
IndexBuffer,
testPlane.indices.data(),
sizeof(Index),
static_cast<uint32_t>(testPlane.indices.size())
);
auto plane = std::make_shared<MeshEntity>();
plane->SetBuffers(planeVertexBuffer, planeIndexBuffer);
plane->SetPipeline(RenderPipeline::GEO_P_C);
world->AddEntity(plane);
}
~MySystem()
{
}
void OnFrame() override
{
m_camera->HandleInput(g_engine->GetFrameInput());
// TODO: model matrix buffer
// Move the spheres around
/*
for (unsigned int i = 0; i < m_spheres.size(); i++)
{
auto pos = m_spheres[i]->GetPosition();
pos.z = static_cast<float>(std::sin(legs::Time::Uptime() * i));
m_spheres[i]->SetPosition(pos);
}
*/
// Move the sun across the sky
const auto degreesPerSecond = 5.0;
auto sunRotation = glm::angleAxis(
glm::radians(degreesPerSecond * legs::Time::Uptime()),
glm::normalize(glm::dvec3 {1.0, 0.3, 0.2})
);
g_engine->GetWorld()->GetSky()->SunDirection =
glm::normalize(sunRotation * glm::dvec3(0.1, 0.2, 1.0));
}
void OnTick() override
{
}
private:
std::shared_ptr<NoclipCamera> m_camera;
std::vector<std::shared_ptr<MeshEntity>> m_spheres;
};
int main(int argc, char** argv)
{
Log::SetLogLevel(LogLevel::Debug);
auto code = LEGS_Init(argc, argv);
if (code < 0)
{
return code;
}
g_engine->GetWindow()->SetTitle("02_systems");
g_engine->AddSystem(std::make_shared<MySystem>());
return LEGS_Run();
}

View file

@ -0,0 +1 @@
executable('02_systems', files('main.cpp'), dependencies: [legs_dep])

View file

@ -0,0 +1,140 @@
#include <memory>
#include <legs/entry.hpp>
#include <legs/entity/physics_entity.hpp>
#include <legs/geometry/icosphere.hpp>
#include <legs/geometry/plane.hpp>
#include <legs/isystem.hpp>
#include <legs/log.hpp>
#include <legs/time.hpp>
using namespace legs;
class MySystem : public ISystem
{
public:
MySystem()
{
int width;
int height;
g_engine->GetWindow()->GetFramebufferSize(&width, &height);
m_camera = std::make_shared<NoclipCamera>(width, height);
m_camera->SetPosition({0.0f, -10.0f, 5.0f});
g_engine->SetCamera(m_camera);
auto renderer = g_engine->GetRenderer();
auto world = g_engine->GetWorld();
// Create a plane
std::shared_ptr<Buffer> planeVertexBuffer;
std::shared_ptr<Buffer> planeIndexBuffer;
auto testPlane = SPlane({0.0f, 0.0f, 0.0f}, 20.0f);
auto planeVertices = std::array<Vertex_P_C, 4>();
for (unsigned int i = 0; i < 4; i++)
{
auto color = glm::vec3 {
i == 0 ? 1.0f : 0.0f,
i == 1 ? 1.0f : 0.0f,
i == 2 ? 1.0f : 0.0f,
};
planeVertices[i] = {testPlane.vertices[i], color};
}
renderer->CreateBuffer(
planeVertexBuffer,
VertexBuffer,
planeVertices.data(),
sizeof(Vertex_P_C),
static_cast<uint32_t>(planeVertices.size())
);
renderer->CreateBuffer(
planeIndexBuffer,
IndexBuffer,
testPlane.indices.data(),
sizeof(Index),
static_cast<uint32_t>(testPlane.indices.size())
);
auto plane = std::make_shared<PhysicsEntity>();
plane->SetBuffers(planeVertexBuffer, planeIndexBuffer);
plane->SetPipeline(RenderPipeline::GEO_P_C);
auto planeCollider = BoxCollider(
JPH::EMotionType::Static,
Layers::NON_MOVING,
plane->GetTransform(),
{20.0f, 20.0f, 0.1f}
);
plane->SetCollider(planeCollider);
world->AddEntity(plane);
// Create a sphere
auto testSphere = SIcosphere(glm::vec3(0.0f, 0.0f, 10.0f), 0.5f, 1);
std::shared_ptr<Buffer> sphereVertexBuffer;
std::shared_ptr<Buffer> sphereIndexBuffer;
std::vector<Vertex_P_N_C> sphereVertices;
sphereVertices.reserve(testSphere.positions.size());
for (unsigned int i = 0; i < testSphere.positions.size(); i++)
{
sphereVertices.push_back(
{testSphere.positions[i], testSphere.normals[i], glm::vec3(0.5, 0.5, 0.5)}
);
}
renderer->CreateBuffer(
sphereVertexBuffer,
VertexBuffer,
sphereVertices.data(),
sizeof(Vertex_P_N_C),
static_cast<uint32_t>(sphereVertices.size())
);
renderer->CreateBuffer(
sphereIndexBuffer,
IndexBuffer,
testSphere.indices.data(),
sizeof(Index),
static_cast<uint32_t>(testSphere.indices.size())
);
auto sphere = std::make_shared<PhysicsEntity>();
sphere->SetBuffers(sphereVertexBuffer, sphereIndexBuffer);
sphere->SetPipeline(RenderPipeline::GEO_P_N_C);
auto sphereCollider =
SphereCollider(JPH::EMotionType::Dynamic, Layers::MOVING, sphere->GetTransform(), 0.5f);
sphere->SetCollider(sphereCollider);
world->AddEntity(sphere);
}
~MySystem()
{
}
void OnFrame() override
{
m_camera->HandleInput(g_engine->GetFrameInput());
}
private:
std::shared_ptr<NoclipCamera> m_camera;
};
int main(int argc, char** argv)
{
Log::SetLogLevel(LogLevel::Debug);
auto code = LEGS_Init(argc, argv);
if (code < 0)
{
return code;
}
g_engine->GetWindow()->SetTitle("03_systems");
g_engine->AddSystem(std::make_shared<MySystem>());
return LEGS_Run();
}

View file

@ -0,0 +1 @@
executable('03_physics', files('main.cpp'), dependencies: [legs_dep])

3
examples/meson.build Normal file
View file

@ -0,0 +1,3 @@
subdir('01_hello_world')
subdir('02_systems')
subdir('03_physics')

1
include/JoltPhysics Submodule

@ -0,0 +1 @@
Subproject commit cede24d2733a4a473c6d486650ca9b6d0481681a

@ -0,0 +1 @@
Subproject commit 1c35ba99ce775f8342d87a83a3f0f696f99c2a39

1
include/glm Submodule

@ -0,0 +1 @@
Subproject commit 33b4a621a697a305bc3a7610d290677b96beb181

1
include/imgui Submodule

@ -0,0 +1 @@
Subproject commit 71714eab5367d2fa119e36be30148278e0d5974f

119
meson.build Normal file
View file

@ -0,0 +1,119 @@
project(
'legs',
'cpp',
default_options: [
'cpp_std=c++23',
'b_ndebug=if-release',
'warning_level=3',
],
meson_version: '>=1.1',
)
cpp = meson.get_compiler('cpp')
buildtype = get_option('buildtype')
compiler_args = [
'-Wcast-align',
'-Wconversion',
'-Wdouble-promotion',
'-Wduplicated-branches',
'-Wduplicated-cond',
'-Wformat=2',
'-Wimplicit-fallthrough',
'-Wlogical-op',
'-Wmisleading-indentation',
'-Wnon-virtual-dtor',
'-Wnrvo',
'-Wnull-dereference',
'-Wold-style-cast',
'-Woverloaded-virtual',
'-Wshadow',
'-Wsign-conversion',
'-Wsuggest-override',
'-Wunused',
'-Wuseless-cast',
'-DGLM_FORCE_DEPTH_ZERO_TO_ONE',
'-m64',
'-DLINUX',
'-D_LINUX',
'-DPOSIX',
'-DJPH_OBJECT_STREAM',
'-DCPP_RTTI_ENABLED',
]
linker_args = [
'-m64',
]
if buildtype == 'debug'
compiler_args += [
'-DJPH_SHARED_LIBRARY',
'-DJPH_PROFILE_ENABLED',
'-DJPH_DEBUG_RENDERER',
]
endif
add_project_arguments(cpp.get_supported_arguments(compiler_args), language: 'cpp')
add_project_link_arguments(cpp.get_supported_link_arguments(linker_args), language: 'cpp')
# https://github.com/mesonbuild/meson/issues/13601
# jolt_opt = cmake.subproject_options()
# jolt_dep = cmake.subproject('Jolt', options: jolt_opt)
jolt_dep = cpp.find_library('Jolt', dirs: [meson.current_source_dir() + '/lib'])
# TODO: is there a way to disable warnings for these?
imgui_src = files(
'include/imgui/backends/imgui_impl_sdl2.cpp',
'include/imgui/backends/imgui_impl_vulkan.cpp',
'include/imgui/imgui.cpp',
'include/imgui/imgui_demo.cpp',
'include/imgui/imgui_draw.cpp',
'include/imgui/imgui_tables.cpp',
'include/imgui/imgui_widgets.cpp',
)
glsl_compiler = find_program('glslang', 'glslangValidator')
glsl_args = [
'--target-env', 'vulkan1.3',
'--vn', '@BASENAME@',
'--depfile', '@DEPFILE@',
'@INPUT@',
'-o', '@OUTPUT@',
]
glsl_generator = generator(
glsl_compiler,
output: ['@BASENAME@.h'],
depfile: '@BASENAME@.h.d',
arguments: glsl_args,
)
legs_includes = include_directories(
'include/VulkanMemoryAllocator/include',
'include/glm',
'include/JoltPhysics',
'include/imgui',
'include/imgui/backends',
is_system: true,
)
legs_version = vcs_tag(
command: ['git', 'describe', '--always', '--dirty=+'],
input: 'version.h.in',
output: 'version.h',
)
subdir('src')
examples = get_option('examples')
if examples == true
subdir('examples')
endif

6
meson.options Normal file
View file

@ -0,0 +1,6 @@
option(
'examples',
description: 'Should the examples be built?',
type: 'boolean',
value: false
)

27
scripts/compile_jolt.sh Executable file
View file

@ -0,0 +1,27 @@
#!/bin/bash -x
set -euo pipefail
mode=${1:-"Debug"}
CWD="$(pwd)"
mkdir -p lib
cd include/JoltPhysics/Build
rm -rf Linux_Debug
rm -rf Linux_Release
rm -rf Linux_Distribution
./cmake_linux_clang_gcc.sh "$mode" clang++ \
-DBUILD_SHARED_LIBS=ON \
-DCPP_RTTI_ENABLED=ON
cd "Linux_$mode"
make -j "$(nproc)"
./UnitTests
cp libJolt.so "$CWD/lib/libJolt.so"

11
scripts/get_steamworks_sdk.sh Executable file
View file

@ -0,0 +1,11 @@
#!/bin/bash
set -euo pipefail
SDK_VER=160
rm -rf include/steamworks
curl "https://partner.steamgames.com/downloads/steamworks_sdk_$SDK_VER.zip" -o /tmp/steamworks.zip
unzip /tmp/steamworks.zip -d include/steamworks
head -n 32 include/steamworks/sdk/Readme.txt

8
scripts/run.sh Executable file
View file

@ -0,0 +1,8 @@
#!/bin/bash
set -euo pipefail
cd build
meson compile
LSAN_OPTIONS="suppressions=../suppr.txt" ./examples/02_systems/02_systems

15
scripts/setup_debug.sh Executable file
View file

@ -0,0 +1,15 @@
#!/bin/bash
set -euo pipefail
# TODO: gcc busted
export CXX=clang++
# TODO: ubsan doesn't link with jolt
meson setup --reconfigure \
-D examples=true \
-D buildtype=debug \
-D b_sanitize=address \
-D b_lundef=false \
build

9
scripts/setup_release.sh Executable file
View file

@ -0,0 +1,9 @@
#!/bin/bash
set -euo pipefail
meson setup --reconfigure \
-D examples=true \
-D buildtype=release \
build

290
src/engine.cpp Normal file
View file

@ -0,0 +1,290 @@
#include <functional>
#include <memory>
#include <stop_token>
#include <thread>
#include <imgui.h>
#include "physics.hpp"
#include "legs/engine.hpp"
#include "legs/log.hpp"
#include "legs/time.hpp"
#include "legs/window/window.hpp"
namespace legs
{
Engine::Engine()
{
LOG_INFO("Creating Engine");
m_inputSettings = std::make_shared<InputSettings>();
m_window = std::make_shared<Window>(m_inputSettings);
m_renderer = std::make_shared<Renderer>(m_window);
int width;
int height;
m_window->GetFramebufferSize(&width, &height);
m_camera = std::make_shared<Camera>(width, height);
m_ui = std::make_unique<UI>(m_window, m_renderer);
Physics::Register();
m_world = std::make_shared<World>(m_renderer);
m_window->SetMouseGrab(true);
auto fps = m_window->GetRefreshRate();
LOG_DEBUG("Setting framerate to {}", fps);
Time::SetFrameRate(fps);
Time::SetStart();
m_frameInput.Clear();
m_tickInput.Clear();
m_tickThread = std::jthread {std::bind_front(&Engine::TickThread, this)};
m_renderThread = std::jthread {std::bind_front(&Engine::RenderThread, this)};
}
Engine::~Engine()
{
LOG_INFO("Destroying Engine");
LOG_DEBUG("Requesting TickThread stop");
m_tickThread.request_stop();
m_threadTickSemaphore.release();
m_tickThread.join();
LOG_DEBUG("Requesting RenderThread stop");
m_renderThread.request_stop();
m_threadFrameSemaphore.release();
m_renderThread.join();
LOG_DEBUG("Waiting for renderer idle");
m_renderer->WaitForIdle();
}
int Engine::Run()
{
m_mainFrameSemaphore.release();
m_mainTickSemaphore.release();
Time::UpdateTickDelta();
Time::UpdateFrameDelta();
const double sleepThreshold = 0.0001;
while (true)
{
auto toTick = Time::TimeToEngineTick();
if (toTick <= 0)
{
if (!Tick())
{
LOG_INFO("Engine::Tick exit");
break;
}
toTick = Time::TickInterval;
}
auto toFrame = Time::TimeToEngineFrame();
if (toFrame <= 0)
{
Frame();
toFrame = Time::FrameInterval;
}
const auto lowest = std::min(toTick, toFrame);
if (lowest < sleepThreshold)
{
std::this_thread::yield();
}
else
{
std::this_thread::sleep_for(Time::Duration(lowest - sleepThreshold));
}
}
return 0;
}
void Engine::Frame()
{
// Renderer still busy?
auto acquired = m_mainFrameSemaphore.try_acquire();
if (!acquired)
{
return;
}
Time::UpdateFrameDelta();
UpdateInput();
for (auto system : m_systems)
{
system->OnFrame();
}
if (m_frameInput.wantsResize)
{
int width;
int height;
m_window->GetFramebufferSize(&width, &height);
m_camera->UpdateViewport(width, height);
m_renderer->Resize();
}
if (m_world != nullptr)
{
m_world->Frame();
}
if (m_frameInput.HasKey(Key::KEY_MOUSE_GRAB))
{
m_window->SetMouseGrab(!m_window->IsMouseGrabbed());
m_frameInput.Clear(true);
}
if (m_frameInput.HasKey(Key::KEY_WINDOW_DEBUG))
{
m_ui->ToggleWindow(UIWindow::DEBUG);
m_frameInput.KeyUp(Key::KEY_WINDOW_DEBUG);
}
if (m_frameInput.HasKey(Key::KEY_WINDOW_DEMO))
{
m_window->SetMouseGrab(false);
m_frameInput.Clear();
m_ui->ToggleWindow(UIWindow::DEMO);
}
m_frameInput.Clear();
// Allow render thread to run.
m_threadFrameSemaphore.release();
}
bool Engine::Tick()
{
if (m_tickInput.wantsQuit)
{
return false;
}
// Tick thread still busy?
auto acquired = m_mainTickSemaphore.try_acquire();
if (!acquired)
{
return true;
}
const auto delta = Time::TimeSinceEngineTick();
const auto slowThreshold = Time::TickInterval * 2.0;
if (delta > slowThreshold)
{
LOG_WARN("Tick thread ran slow: {:.2f}ms", 1000 * Time::TimeSinceEngineTick());
}
Time::UpdateTickDelta();
// Allow tick thread to run.
m_threadTickSemaphore.release();
return true;
}
void Engine::UpdateInput()
{
m_window->AggregateInput(m_frameInput);
m_tickInput.Aggregate(m_frameInput);
}
void Engine::TickThread(const std::stop_token token)
{
LOG_INFO("Enter TickThread");
while (!token.stop_requested())
{
// Wait for main thread.
m_threadTickSemaphore.acquire();
for (auto system : m_systems)
{
system->OnTick();
}
if (m_world != nullptr)
{
m_world->Tick();
}
m_tickInput.Clear();
// Let main thread know we are done.
m_mainTickSemaphore.release();
}
}
void Engine::RenderThread(const std::stop_token token)
{
LOG_INFO("Enter RenderThread");
while (!token.stop_requested())
{
// Wait for main thread.
m_threadFrameSemaphore.acquire();
Time::StartRender();
if (m_window->IsMinimized())
{
Time::StopRender();
m_mainFrameSemaphore.release();
continue;
}
m_renderer->Begin();
auto ubo = m_renderer->GetUBO();
ubo->sunColor = {};
ubo->sunDir = {};
if (m_world != nullptr)
{
if (auto sky = m_world->GetSky())
{
ubo->sunDir = sky->SunDirection;
ubo->sunColor = sky->SunColor;
}
}
if (m_camera != nullptr)
{
ubo->SetCamera(m_camera);
}
m_renderer->UpdateUBO();
if (m_world != nullptr)
{
m_world->Render();
}
m_ui->Render();
m_renderer->Submit();
m_renderer->Present();
Time::StopRender();
// Let main thread know we are done.
m_mainFrameSemaphore.release();
}
LOG_INFO("Exit RenderThread");
}
} // namespace legs

8
src/entry.cpp Normal file
View file

@ -0,0 +1,8 @@
#include <memory>
#include <legs/entry.hpp>
namespace legs
{
std::shared_ptr<Engine> g_engine;
}

View file

@ -0,0 +1,337 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <Jolt/Jolt.h>
#include <Jolt/Core/FPException.h>
#include <Jolt/Core/Profiler.h>
#include "job_system_thread_pool.hpp"
#ifdef JPH_PLATFORM_LINUX
#include <sys/prctl.h>
#endif
using namespace JPH;
namespace legs
{
void JobSystemThreadPool::Init(uint inMaxJobs, uint inMaxBarriers, int inNumThreads)
{
JobSystemWithBarrier::Init(inMaxBarriers);
// Init freelist of jobs
mJobs.Init(inMaxJobs, inMaxJobs);
// Init queue
for (std::atomic<Job*>& j : mQueue)
{
j = nullptr;
}
// Start the worker threads
StartThreads(inNumThreads);
}
JobSystemThreadPool::JobSystemThreadPool(uint inMaxJobs, uint inMaxBarriers, int inNumThreads)
{
Init(inMaxJobs, inMaxBarriers, inNumThreads);
}
void JobSystemThreadPool::StartThreads([[maybe_unused]] int inNumThreads)
{
#if !defined(JPH_CPU_WASM) \
|| defined(__EMSCRIPTEN_PTHREADS__) // If we're running without threads support we cannot create
// threads and we ignore the inNumThreads parameter
// Auto detect number of threads
if (inNumThreads < 0)
{
inNumThreads = std::thread::hardware_concurrency() - 1;
}
// If no threads are requested we're done
if (inNumThreads == 0)
{
return;
}
// Don't quit the threads
mQuit = false;
// Allocate heads
mHeads =
reinterpret_cast<std::atomic<uint>*>(JPH::Allocate(sizeof(std::atomic<uint>) * inNumThreads)
);
for (int i = 0; i < inNumThreads; ++i)
{
mHeads[i] = 0;
}
// Start running threads
JPH_ASSERT(mThreads.empty());
mThreads.reserve(inNumThreads);
for (int i = 0; i < inNumThreads; ++i)
{
mThreads.emplace_back([this, i] { ThreadMain(i); });
}
#endif
}
JobSystemThreadPool::~JobSystemThreadPool()
{
// Stop all worker threads
StopThreads();
}
void JobSystemThreadPool::StopThreads()
{
if (mThreads.empty())
{
return;
}
// Signal threads that we want to stop and wake them up
mQuit = true;
mSemaphore.Release((uint)mThreads.size());
// Wait for all threads to finish
for (std::thread& t : mThreads)
{
if (t.joinable())
{
t.join();
}
}
// Delete all threads
mThreads.clear();
// Ensure that there are no lingering jobs in the queue
for (uint head = 0; head != mTail; ++head)
{
// Fetch job
Job* job_ptr = mQueue[head & (cQueueLength - 1)].exchange(nullptr);
if (job_ptr != nullptr)
{
// And execute it
job_ptr->Execute();
job_ptr->Release();
}
}
// Destroy heads and reset tail
JPH::Free(mHeads);
mHeads = nullptr;
mTail = 0;
}
JPH::JobHandle JobSystemThreadPool::CreateJob(
const char* inJobName,
JPH::ColorArg inColor,
const JobFunction& inJobFunction,
uint32_t inNumDependencies
)
{
JPH_PROFILE_FUNCTION();
// Loop until we can get a job from the free list
uint32_t index;
for (;;)
{
index = mJobs.ConstructObject(inJobName, inColor, this, inJobFunction, inNumDependencies);
if (index != AvailableJobs::cInvalidObjectIndex)
{
break;
}
JPH_ASSERT(false, "No jobs available!");
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
Job* job = &mJobs.Get(index);
// Construct handle to keep a reference, the job is queued below and may immediately complete
JobHandle handle(job);
// If there are no dependencies, queue the job now
if (inNumDependencies == 0)
{
QueueJob(job);
}
// Return the handle
return handle;
}
void JobSystemThreadPool::FreeJob(Job* inJob)
{
mJobs.DestructObject(inJob);
}
uint JobSystemThreadPool::GetHead() const
{
// Find the minimal value across all threads
uint head = mTail;
for (size_t i = 0; i < mThreads.size(); ++i)
{
head = std::min(head, mHeads[i].load());
}
return head;
}
void JobSystemThreadPool::QueueJobInternal(Job* inJob)
{
// Add reference to job because we're adding the job to the queue
inJob->AddRef();
// Need to read head first because otherwise the tail can already have passed the head
// We read the head outside of the loop since it involves iterating over all threads and we only
// need to update it if there's not enough space in the queue.
uint head = GetHead();
for (;;)
{
// Check if there's space in the queue
uint old_value = mTail;
if (old_value - head >= cQueueLength)
{
// We calculated the head outside of the loop, update head (and we also need to update
// tail to prevent it from passing head)
head = GetHead();
old_value = mTail;
// Second check if there's space in the queue
if (old_value - head >= cQueueLength)
{
// Wake up all threads in order to ensure that they can clear any nullptrs they may
// not have processed yet
mSemaphore.Release((uint)mThreads.size());
// Sleep a little (we have to wait for other threads to update their head pointer in
// order for us to be able to continue)
std::this_thread::sleep_for(std::chrono::microseconds(100));
continue;
}
}
// Write the job pointer if the slot is empty
Job* expected_job = nullptr;
bool success =
mQueue[old_value & (cQueueLength - 1)].compare_exchange_strong(expected_job, inJob);
// Regardless of who wrote the slot, we will update the tail (if the successful thread got
// scheduled out after writing the pointer we still want to be able to continue)
mTail.compare_exchange_strong(old_value, old_value + 1);
// If we successfully added our job we're done
if (success)
{
break;
}
}
}
void JobSystemThreadPool::QueueJob(Job* inJob)
{
JPH_PROFILE_FUNCTION();
// If we have no worker threads, we can't queue the job either. We assume in this case that the
// job will be added to a barrier and that the barrier will execute the job when it's Wait()
// function is called.
if (mThreads.empty())
{
return;
}
// Queue the job
QueueJobInternal(inJob);
// Wake up thread
mSemaphore.Release();
}
void JobSystemThreadPool::QueueJobs(Job** inJobs, uint inNumJobs)
{
JPH_PROFILE_FUNCTION();
JPH_ASSERT(inNumJobs > 0);
// If we have no worker threads, we can't queue the job either. We assume in this case that the
// job will be added to a barrier and that the barrier will execute the job when it's Wait()
// function is called.
if (mThreads.empty())
{
return;
}
// Queue all jobs
for (Job **job = inJobs, **job_end = inJobs + inNumJobs; job < job_end; ++job)
{
QueueJobInternal(*job);
}
// Wake up threads
mSemaphore.Release(std::min(inNumJobs, (uint)mThreads.size()));
}
static void SetThreadName(const char* inName)
{
JPH_ASSERT(strlen(inName) < 16); // String will be truncated if it is longer
prctl(PR_SET_NAME, inName, 0, 0, 0);
}
void JobSystemThreadPool::ThreadMain(int inThreadIndex)
{
// Name the thread
char name[64];
snprintf(name, sizeof(name), "Worker %d", int(inThreadIndex + 1));
#if defined(JPH_PLATFORM_WINDOWS) || defined(JPH_PLATFORM_LINUX)
SetThreadName(name);
#endif // JPH_PLATFORM_WINDOWS && !JPH_COMPILER_MINGW
// Enable floating point exceptions
JPH::FPExceptionsEnable enable_exceptions;
JPH_UNUSED(enable_exceptions);
JPH_PROFILE_THREAD_START(name);
// Call the thread init function
mThreadInitFunction(inThreadIndex);
std::atomic<uint>& head = mHeads[inThreadIndex];
while (!mQuit)
{
// Wait for jobs
mSemaphore.Acquire();
{
JPH_PROFILE("Executing Jobs");
// Loop over the queue
while (head != mTail)
{
// Exchange any job pointer we find with a nullptr
std::atomic<Job*>& job = mQueue[head & (cQueueLength - 1)];
if (job.load() != nullptr)
{
Job* job_ptr = job.exchange(nullptr);
if (job_ptr != nullptr)
{
// And execute it
job_ptr->Execute();
job_ptr->Release();
}
}
head++;
}
}
}
// Call the thread exit function
mThreadExitFunction(inThreadIndex);
JPH_PROFILE_THREAD_END();
}
}; // namespace legs

View file

@ -0,0 +1,119 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <thread>
#include <legs/jolt_pch.hpp>
#include "job_system_with_barrier.hpp"
namespace legs
{
/// Implementation of a JobSystem using a thread pool
///
/// Note that this is considered an example implementation. It is expected that when you integrate
/// the physics engine into your own project that you'll provide your own implementation of the
/// JobSystem built on top of whatever job system your project uses.
class JobSystemThreadPool final : public JobSystemWithBarrier
{
public:
/// Creates a thread pool.
/// @see JobSystemThreadPool::Init
JobSystemThreadPool(uint inMaxJobs, uint inMaxBarriers, int inNumThreads = -1);
JobSystemThreadPool() = default;
virtual ~JobSystemThreadPool() override;
/// Functions to call when a thread is initialized or exits, must be set before calling Init()
using InitExitFunction = JPH::function<void(int)>;
void SetThreadInitFunction(const InitExitFunction& inInitFunction)
{
mThreadInitFunction = inInitFunction;
}
void SetThreadExitFunction(const InitExitFunction& inExitFunction)
{
mThreadExitFunction = inExitFunction;
}
/// Initialize the thread pool
/// @param inMaxJobs Max number of jobs that can be allocated at any time
/// @param inMaxBarriers Max number of barriers that can be allocated at any time
/// @param inNumThreads Number of threads to start (the number of concurrent jobs is 1 more
/// because the main thread will also run jobs while waiting for a barrier to complete). Use -1
/// to auto detect the amount of CPU's.
void Init(uint inMaxJobs, uint inMaxBarriers, int inNumThreads = -1);
// See JobSystem
virtual int GetMaxConcurrency() const override
{
return int(mThreads.size()) + 1;
}
virtual JobHandle CreateJob(
const char* inName,
JPH::ColorArg inColor,
const JobFunction& inJobFunction,
uint32_t inNumDependencies = 0
) override;
/// Change the max concurrency after initialization
void SetNumThreads(int inNumThreads)
{
StopThreads();
StartThreads(inNumThreads);
}
protected:
// See JobSystem
virtual void QueueJob(Job* inJob) override;
virtual void QueueJobs(Job** inJobs, uint inNumJobs) override;
virtual void FreeJob(Job* inJob) override;
private:
/// Start/stop the worker threads
void StartThreads(int inNumThreads);
void StopThreads();
/// Entry point for a thread
void ThreadMain(int inThreadIndex);
/// Get the head of the thread that has processed the least amount of jobs
inline uint GetHead() const;
/// Internal helper function to queue a job
inline void QueueJobInternal(Job* inJob);
/// Functions to call when initializing or exiting a thread
InitExitFunction mThreadInitFunction = [](int) {};
InitExitFunction mThreadExitFunction = [](int) {};
/// Array of jobs (fixed size)
using AvailableJobs = JPH::FixedSizeFreeList<Job>;
AvailableJobs mJobs;
/// Threads running jobs
JPH::Array<std::thread> mThreads;
// The job queue
static constexpr uint32_t cQueueLength = 1024;
static_assert(JPH::IsPowerOf2(cQueueLength)
); // We do bit operations and require queue length to be a power of 2
std::atomic<Job*> mQueue[cQueueLength];
// Head and tail of the queue, do this value modulo cQueueLength - 1 to get the element in the
// mQueue array
std::atomic<uint>* mHeads = nullptr; ///< Per executing thread the head of the current queue
alignas(JPH_CACHE_LINE_SIZE) std::atomic<uint> mTail = 0; ///< Tail (write end) of the queue
// Semaphore used to signal worker threads that there is new work
JPH::Semaphore mSemaphore;
/// Boolean to indicate that we want to stop the job system
std::atomic<bool> mQuit = false;
};
}; // namespace legs

View file

@ -0,0 +1,253 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#include <thread>
#include <Jolt/Jolt.h>
#include <Jolt/Core/JobSystemWithBarrier.h>
#include <Jolt/Core/Profiler.h>
#include "job_system_with_barrier.hpp"
using namespace JPH;
namespace legs
{
JobSystemWithBarrier::BarrierImpl::BarrierImpl()
{
for (std::atomic<Job*>& j : mJobs)
{
j = nullptr;
}
}
JobSystemWithBarrier::BarrierImpl::~BarrierImpl()
{
JPH_ASSERT(IsEmpty());
}
void JobSystemWithBarrier::BarrierImpl::AddJob(const JobHandle& inJob)
{
JPH_PROFILE_FUNCTION();
bool release_semaphore = false;
// Set the barrier on the job, this returns true if the barrier was successfully set (otherwise
// the job is already done and we don't need to add it to our list)
Job* job = inJob.GetPtr();
if (job->SetBarrier(this))
{
// If the job can be executed we want to release the semaphore an extra time to allow the
// waiting thread to start executing it
mNumToAcquire++;
if (job->CanBeExecuted())
{
release_semaphore = true;
mNumToAcquire++;
}
// Add the job to our job list
job->AddRef();
uint write_index = mJobWriteIndex++;
while (write_index - mJobReadIndex >= cMaxJobs)
{
JPH_ASSERT(false, "Barrier full, stalling!");
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
mJobs[write_index & (cMaxJobs - 1)] = job;
}
// Notify waiting thread that a new executable job is available
if (release_semaphore)
{
mSemaphore.Release();
}
}
void JobSystemWithBarrier::BarrierImpl::AddJobs(const JobHandle* inHandles, uint inNumHandles)
{
JPH_PROFILE_FUNCTION();
bool release_semaphore = false;
for (const JobHandle *handle = inHandles, *handles_end = inHandles + inNumHandles;
handle < handles_end;
++handle)
{
// Set the barrier on the job, this returns true if the barrier was successfully set
// (otherwise the job is already done and we don't need to add it to our list)
Job* job = handle->GetPtr();
if (job->SetBarrier(this))
{
// If the job can be executed we want to release the semaphore an extra time to allow
// the waiting thread to start executing it
mNumToAcquire++;
if (!release_semaphore && job->CanBeExecuted())
{
release_semaphore = true;
mNumToAcquire++;
}
// Add the job to our job list
job->AddRef();
uint write_index = mJobWriteIndex++;
while (write_index - mJobReadIndex >= cMaxJobs)
{
JPH_ASSERT(false, "Barrier full, stalling!");
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
mJobs[write_index & (cMaxJobs - 1)] = job;
}
}
// Notify waiting thread that a new executable job is available
if (release_semaphore)
{
mSemaphore.Release();
}
}
void JobSystemWithBarrier::BarrierImpl::OnJobFinished(Job* inJob)
{
JPH_PROFILE_FUNCTION();
mSemaphore.Release();
}
void JobSystemWithBarrier::BarrierImpl::Wait()
{
while (mNumToAcquire > 0)
{
{
JPH_PROFILE("Execute Jobs");
// Go through all jobs
bool has_executed;
do
{
has_executed = false;
// Loop through the jobs and erase jobs from the beginning of the list that are done
while (mJobReadIndex < mJobWriteIndex)
{
std::atomic<Job*>& job = mJobs[mJobReadIndex & (cMaxJobs - 1)];
Job* job_ptr = job.load();
if (job_ptr == nullptr || !job_ptr->IsDone())
{
break;
}
// Job is finished, release it
job_ptr->Release();
job = nullptr;
++mJobReadIndex;
}
// Loop through the jobs and execute the first executable job
for (uint index = mJobReadIndex; index < mJobWriteIndex; ++index)
{
const std::atomic<Job*>& job = mJobs[index & (cMaxJobs - 1)];
Job* job_ptr = job.load();
if (job_ptr != nullptr && job_ptr->CanBeExecuted())
{
// This will only execute the job if it has not already executed
job_ptr->Execute();
has_executed = true;
break;
}
}
} while (has_executed);
}
// Wait for another thread to wake us when either there is more work to do or when all jobs
// have completed
int num_to_acquire = std::max(
1,
mSemaphore.GetValue()
); // When there have been multiple releases, we acquire them all at the same time to
// avoid needlessly spinning on executing jobs
mSemaphore.Acquire(num_to_acquire);
mNumToAcquire -= num_to_acquire;
}
// All jobs should be done now, release them
while (mJobReadIndex < mJobWriteIndex)
{
std::atomic<Job*>& job = mJobs[mJobReadIndex & (cMaxJobs - 1)];
Job* job_ptr = job.load();
JPH_ASSERT(job_ptr != nullptr && job_ptr->IsDone());
job_ptr->Release();
job = nullptr;
++mJobReadIndex;
}
}
void JobSystemWithBarrier::Init(uint inMaxBarriers)
{
JPH_ASSERT(mBarriers == nullptr); // Already initialized?
// Init freelist of barriers
mMaxBarriers = inMaxBarriers;
mBarriers = new BarrierImpl[inMaxBarriers];
}
JobSystemWithBarrier::JobSystemWithBarrier(uint inMaxBarriers)
{
Init(inMaxBarriers);
}
JobSystemWithBarrier::~JobSystemWithBarrier()
{
// Ensure that none of the barriers are used
#ifdef JPH_ENABLE_ASSERTS
for (const BarrierImpl *b = mBarriers, *b_end = mBarriers + mMaxBarriers; b < b_end; ++b)
{
JPH_ASSERT(!b->mInUse);
}
#endif // JPH_ENABLE_ASSERTS
delete[] mBarriers;
}
JPH::JobSystem::Barrier* JobSystemWithBarrier::CreateBarrier()
{
JPH_PROFILE_FUNCTION();
// Find the first unused barrier
for (uint32_t index = 0; index < mMaxBarriers; ++index)
{
bool expected = false;
if (mBarriers[index].mInUse.compare_exchange_strong(expected, true))
{
return &mBarriers[index];
}
}
return nullptr;
}
void JobSystemWithBarrier::DestroyBarrier(Barrier* inBarrier)
{
JPH_PROFILE_FUNCTION();
// Check that no jobs are in the barrier
JPH_ASSERT(static_cast<BarrierImpl*>(inBarrier)->IsEmpty());
// Flag the barrier as unused
bool expected = true;
static_cast<BarrierImpl*>(inBarrier)->mInUse.compare_exchange_strong(expected, false);
JPH_ASSERT(expected);
}
void JobSystemWithBarrier::WaitForJobs(Barrier* inBarrier)
{
JPH_PROFILE_FUNCTION();
// Let our barrier implementation wait for the jobs
static_cast<BarrierImpl*>(inBarrier)->Wait();
}
}; // namespace legs

View file

@ -0,0 +1,95 @@
// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2023 Jorrit Rouwe
// SPDX-License-Identifier: MIT
#pragma once
#include <legs/jolt_pch.hpp>
namespace legs
{
/// Implementation of the Barrier class for a JobSystem
///
/// This class can be used to make it easier to create a new JobSystem implementation that
/// integrates with your own job system. It will implement all functionality relating to barriers,
/// so the only functions that are left to be implemented are:
///
/// * JobSystem::GetMaxConcurrency
/// * JobSystem::CreateJob
/// * JobSystem::FreeJob
/// * JobSystem::QueueJob/QueueJobs
///
/// See instructions in JobSystem for more information on how to implement these.
class JobSystemWithBarrier : public JPH::JobSystem
{
public:
/// Constructs barriers
/// @see JobSystemWithBarrier::Init
explicit JobSystemWithBarrier(uint inMaxBarriers);
JobSystemWithBarrier() = default;
virtual ~JobSystemWithBarrier() override;
/// Initialize the barriers
/// @param inMaxBarriers Max number of barriers that can be allocated at any time
void Init(uint inMaxBarriers);
// See JobSystem
virtual Barrier* CreateBarrier() override;
virtual void DestroyBarrier(Barrier* inBarrier) override;
virtual void WaitForJobs(Barrier* inBarrier) override;
private:
class BarrierImpl : public Barrier
{
public:
/// Constructor
BarrierImpl();
virtual ~BarrierImpl() override;
// See Barrier
virtual void AddJob(const JobHandle& inJob) override;
virtual void AddJobs(const JobHandle* inHandles, uint inNumHandles) override;
/// Check if there are any jobs in the job barrier
inline bool IsEmpty() const
{
return mJobReadIndex == mJobWriteIndex;
}
/// Wait for all jobs in this job barrier, while waiting, execute jobs that are part of this
/// barrier on the current thread
void Wait();
/// Flag to indicate if a barrier has been handed out
std::atomic<bool> mInUse {false};
protected:
/// Called by a Job to mark that it is finished
virtual void OnJobFinished(Job* inJob) override;
/// Jobs queue for the barrier
static constexpr uint cMaxJobs = 2048;
static_assert(JPH::IsPowerOf2(cMaxJobs)
); // We do bit operations and require max jobs to be a power of 2
std::atomic<Job*> mJobs[cMaxJobs]; ///< List of jobs that are part of this barrier, nullptrs
///< for empty slots
alignas(JPH_CACHE_LINE_SIZE) std::atomic<uint> mJobReadIndex {0
}; ///< First job that could be valid (modulo cMaxJobs), can be nullptr if other thread is
///< still working on adding the job
alignas(JPH_CACHE_LINE_SIZE) std::atomic<uint> mJobWriteIndex {0
}; ///< First job that can be written (modulo cMaxJobs)
std::atomic<int> mNumToAcquire {0
}; ///< Number of times the semaphore has been released, the barrier should acquire the
///< semaphore this many times (written at the same time as mJobWriteIndex so ok to put
///< in same cache line)
JPH::Semaphore mSemaphore; ///< Semaphore used by finishing jobs to signal the barrier that
///< they're done
};
/// Array of barriers (we keep them constructed all the time since constructing a
/// semaphore/mutex is not cheap)
uint mMaxBarriers = 0; ///< Max amount of barriers
BarrierImpl* mBarriers = nullptr; ///< List of the actual barriers
};
}; // namespace legs

57
src/meson.build Normal file
View file

@ -0,0 +1,57 @@
sdl_dep = dependency('sdl2', include_type: 'system')
vk_dep = dependency('vulkan', include_type: 'system')
legs_src = files(
'renderer/buffer.cpp',
'renderer/descriptor_set.cpp',
'renderer/device.cpp',
'renderer/instance.cpp',
'renderer/renderer.cpp',
'renderer/vma_usage.cpp',
'ui/ui.cpp',
'window/window.cpp',
'world/world.cpp',
'engine.cpp',
'entry.cpp',
'job_system_thread_pool.cpp',
'job_system_with_barrier.cpp',
'physics.cpp',
)
legs_phc = [
'public/legs/jolt_pch.hpp',
]
legs_shaders = files(
'public/legs/shaders/fullscreen_frag.frag',
'public/legs/shaders/fullscreen_vert.vert',
'public/legs/shaders/lit_pnc_frag.frag',
'public/legs/shaders/lit_pnc_vert.vert',
'public/legs/shaders/sky_frag.frag',
'public/legs/shaders/sky_vert.vert',
'public/legs/shaders/unlit_pc_frag.frag',
'public/legs/shaders/unlit_pc_vert.vert',
)
legs_public_includes = include_directories(['public/'])
legs_lib = library(
'legs',
legs_src + imgui_src,
glsl_generator.process(legs_shaders),
legs_version,
link_with: [],
dependencies: [jolt_dep, sdl_dep, vk_dep],
include_directories: [legs_includes, legs_public_includes],
cpp_pch: legs_phc,
)
legs_dep = declare_dependency(
link_with: [legs_lib],
dependencies: [jolt_dep, sdl_dep, vk_dep],
include_directories: [legs_includes, legs_public_includes],
)

224
src/physics.cpp Normal file
View file

@ -0,0 +1,224 @@
#include <cmath>
#include <cstdarg>
#include <iostream>
#include <thread>
#include <legs/time.hpp>
#include "physics.hpp"
namespace legs
{
// Callback for traces, connect this to your own trace function if you have one
static void TraceImpl(const char* fmt, ...)
{
va_list list;
va_start(list, fmt);
char buffer[1024];
vsnprintf(buffer, sizeof(buffer), fmt, list);
va_end(list);
LOG_ERROR("JOLT TRACE: {}", buffer);
}
#ifdef JPH_ENABLE_ASSERTS
// Callback for asserts, connect this to your own assert handler if you have one
static bool AssertFailedImpl(
const char* inExpression,
const char* inMessage,
const char* inFile,
uint inLine
)
{
Log::Print(inFile, inLine, inExpression, LogLevel::Error, inMessage);
// Breakpoint
return true;
};
#endif // JPH_ENABLE_ASSERTS
void Physics::Register()
{
// Register allocation hook. In this example we'll just let Jolt use malloc / free but you can
// override these if you want (see Memory.h). This needs to be done before any other Jolt
// function is called.
JPH::RegisterDefaultAllocator();
// Install trace and assert callbacks
JPH::Trace = TraceImpl;
JPH_IF_ENABLE_ASSERTS(JPH::AssertFailed = AssertFailedImpl;)
// Create a factory, this class is responsible for creating instances of classes based on their
// name or hash and is mainly used for deserialization of saved data. It is not directly used in
// this example but still required.
JPH::Factory::sInstance = new JPH::Factory();
// Register all physics types with the factory and install their collision handlers with the
// CollisionDispatch class. If you have your own custom shape types you probably need to
// register their handlers with the CollisionDispatch before calling this function. If you
// implement your own default material (PhysicsMaterial::sDefault) make sure to initialize it
// before this function or else this function will create one for you.
JPH::RegisterTypes();
}
Physics::Physics() :
m_tempAllocator(10 * 1024 * 1024),
m_jobSystem(
JPH::cMaxPhysicsJobs,
JPH::cMaxPhysicsBarriers,
static_cast<int>(std::thread::hardware_concurrency()) - 1
),
m_maxDeltaTime(1.0f / 60.0f)
{
// This is the max amount of rigid bodies that you can add to the physics system. If you try to
// add more you'll get an error. Note: This value is low because this is a simple test. For a
// real project use something in the order of 65536.
const uint cMaxBodies = 1024;
// This determines how many mutexes to allocate to protect rigid bodies from concurrent access.
// Set it to 0 for the default settings.
const uint cNumBodyMutexes = 0;
// This is the max amount of body pairs that can be queued at any time (the broad phase will
// detect overlapping body pairs based on their bounding boxes and will insert them into a queue
// for the narrowphase). If you make this buffer too small the queue will fill up and the broad
// phase jobs will start to do narrow phase work. This is slightly less efficient. Note: This
// value is low because this is a simple test. For a real project use something in the order of
// 65536.
const uint cMaxBodyPairs = 1024;
// This is the maximum size of the contact constraint buffer. If more contacts (collisions
// between bodies) are detected than this number then these contacts will be ignored and bodies
// will start interpenetrating / fall through the world. Note: This value is low because this is
// a simple test. For a real project use something in the order of 10240.
const uint cMaxContactConstraints = 1024;
// Now we can create the actual physics system.
m_physicsSystem.Init(
cMaxBodies,
cNumBodyMutexes,
cMaxBodyPairs,
cMaxContactConstraints,
m_broadPhaseLayerInterface,
m_objectVsBroadphaseLayerFilter,
m_objectVsObjectLayerFilter
);
// A body activation listener gets notified when bodies activate and go to sleep
// Note that this is called from a job so whatever you do here needs to be thread safe.
// Registering one is entirely optional.
m_physicsSystem.SetBodyActivationListener(&m_bodyActivationListener);
// A contact listener gets notified when bodies (are about to) collide, and when they separate
// again. Note that this is called from a job so whatever you do here needs to be thread safe.
// Registering one is entirely optional.
m_physicsSystem.SetContactListener(&m_contactListener);
}
Physics::~Physics()
{
// Unregisters all types with the factory and cleans up the default material
JPH::UnregisterTypes();
// Destroy the factory
if (JPH::Factory::sInstance != nullptr)
{
delete JPH::Factory::sInstance;
JPH::Factory::sInstance = nullptr;
}
}
void Physics::Optimize()
{
m_physicsSystem.OptimizeBroadPhase();
}
void Physics::Update()
{
const unsigned int steps = std::ceil(Time::DeltaTick / m_maxDeltaTime);
m_physicsSystem.Update(Time::DeltaTick, steps, &m_tempAllocator, &m_jobSystem);
}
JPH::BodyID Physics::CreateBody(JPH::BodyCreationSettings settings)
{
auto body = m_physicsSystem.GetBodyInterface().CreateBody(settings);
if (body != nullptr)
{
return body->GetID();
}
return JPH::BodyID(JPH::BodyID::cInvalidBodyID);
}
void Physics::AddBody(JPH::BodyID id)
{
m_physicsSystem.GetBodyInterface().AddBody(id, JPH::EActivation::Activate);
}
void Physics::RemoveBody(JPH::BodyID id)
{
m_physicsSystem.GetBodyInterface().RemoveBody(id);
}
void Physics::DestroyBody(JPH::BodyID id)
{
m_physicsSystem.GetBodyInterface().DestroyBody(id);
}
void Physics::GetBodyTransform(JPH::BodyID id, std::shared_ptr<STransform> trans)
{
JPH::RVec3 joltPos;
JPH::Quat joltRot;
m_physicsSystem.GetBodyInterface().GetPositionAndRotation(id, joltPos, joltRot);
trans->position.x = joltPos.GetX();
trans->position.y = joltPos.GetY();
trans->position.z = joltPos.GetZ();
trans->rotation.quaternion.x = joltRot.GetX();
trans->rotation.quaternion.y = joltRot.GetY();
trans->rotation.quaternion.z = joltRot.GetZ();
trans->rotation.quaternion.w = joltRot.GetW();
}
void Physics::SetBodyTransform(JPH::BodyID id, std::shared_ptr<STransform> trans)
{
JPH::RVec3 joltPos = {trans->position.x, trans->position.y, trans->position.z};
JPH::Quat joltRot = {
trans->rotation.quaternion.x,
trans->rotation.quaternion.y,
trans->rotation.quaternion.w,
trans->rotation.quaternion.z
};
m_physicsSystem.GetBodyInterface()
.SetPositionAndRotation(id, joltPos, joltRot, JPH::EActivation::Activate);
}
void Physics::SetBodyPosition(JPH::BodyID id, glm::vec3 pos)
{
JPH::RVec3 joltPos = {pos.x, pos.y, pos.z};
m_physicsSystem.GetBodyInterface().SetPosition(id, joltPos, JPH::EActivation::Activate);
}
void Physics::SetBodyRotation(JPH::BodyID id, glm::quat rot)
{
JPH::Quat joltRot = {rot.x, rot.y, rot.w, rot.z};
m_physicsSystem.GetBodyInterface().SetRotation(id, joltRot, JPH::EActivation::Activate);
}
void Physics::SetBodyVelocity(JPH::BodyID id, glm::vec3 vel)
{
JPH::RVec3 joltVel = {vel.x, vel.y, vel.z};
m_physicsSystem.GetBodyInterface().SetPosition(id, joltVel, JPH::EActivation::Activate);
}
void Physics::SetBodyAngularVelocity(JPH::BodyID id, glm::vec3 vel)
{
JPH::RVec3 joltVel = {vel.x, vel.y, vel.z};
m_physicsSystem.GetBodyInterface().SetPosition(id, joltVel, JPH::EActivation::Activate);
}
}; // namespace legs

187
src/physics.hpp Normal file
View file

@ -0,0 +1,187 @@
#pragma once
#include <legs/collider.hpp>
#include <legs/iphysics.hpp>
#include <legs/log.hpp>
#include "job_system_thread_pool.hpp"
namespace legs
{
// Each broadphase layer results in a separate bounding volume tree in the broad phase. You at least
// want to have a layer for non-moving and moving objects to avoid having to update a tree full of
// static objects every frame. You can have a 1-on-1 mapping between object layers and broadphase
// layers (like in this case) but if you have many object layers you'll be creating many broad phase
// trees, which is not efficient. If you want to fine tune your broadphase layers define
// JPH_TRACK_BROADPHASE_STATS and look at the stats reported on the TTY.
namespace BroadPhaseLayers
{
static constexpr JPH::BroadPhaseLayer NON_MOVING(0);
static constexpr JPH::BroadPhaseLayer MOVING(1);
static constexpr uint NUM_LAYERS(2);
}; // namespace BroadPhaseLayers
// BroadPhaseLayerInterface implementation
// This defines a mapping between object and broadphase layers.
class BPLayerInterfaceImpl final : public JPH::BroadPhaseLayerInterface
{
public:
BPLayerInterfaceImpl()
{
// Create a mapping table from object to broad phase layer
mObjectToBroadPhase[Layers::NON_MOVING] = BroadPhaseLayers::NON_MOVING;
mObjectToBroadPhase[Layers::MOVING] = BroadPhaseLayers::MOVING;
}
virtual uint GetNumBroadPhaseLayers() const override
{
return BroadPhaseLayers::NUM_LAYERS;
}
virtual JPH::BroadPhaseLayer GetBroadPhaseLayer(JPH::ObjectLayer inLayer) const override
{
JPH_ASSERT(inLayer < Layers::NUM_LAYERS);
return mObjectToBroadPhase[inLayer];
}
#if defined(JPH_EXTERNAL_PROFILE) || defined(JPH_PROFILE_ENABLED)
virtual const char* GetBroadPhaseLayerName(JPH::BroadPhaseLayer inLayer) const override
{
switch ((JPH::BroadPhaseLayer::Type)inLayer)
{
case (JPH::BroadPhaseLayer::Type)BroadPhaseLayers::NON_MOVING:
return "NON_MOVING";
case (JPH::BroadPhaseLayer::Type)BroadPhaseLayers::MOVING:
return "MOVING";
default:
JPH_ASSERT(false);
return "INVALID";
}
}
#endif // JPH_EXTERNAL_PROFILE || JPH_PROFILE_ENABLED
private:
JPH::BroadPhaseLayer mObjectToBroadPhase[Layers::NUM_LAYERS];
};
/// Class that determines if an object layer can collide with a broadphase layer
class ObjectVsBroadPhaseLayerFilterImpl : public JPH::ObjectVsBroadPhaseLayerFilter
{
public:
virtual bool ShouldCollide(JPH::ObjectLayer inLayer1, JPH::BroadPhaseLayer inLayer2)
const override
{
switch (inLayer1)
{
case Layers::NON_MOVING:
return inLayer2 == BroadPhaseLayers::MOVING;
case Layers::MOVING:
return true;
default:
JPH_ASSERT(false);
return false;
}
}
};
// An example contact listener
class MyContactListener : public JPH::ContactListener
{
public:
// See: ContactListener
virtual JPH::ValidateResult OnContactValidate(
const JPH::Body& inBody1,
const JPH::Body& inBody2,
JPH::RVec3Arg inBaseOffset,
const JPH::CollideShapeResult& inCollisionResult
) override
{
// std::cout << "Contact validate callback" << std::endl;
// Allows you to ignore a contact before it is created (using layers to not make objects
// collide is cheaper!)
return JPH::ValidateResult::AcceptAllContactsForThisBodyPair;
}
virtual void OnContactAdded(
const JPH::Body& inBody1,
const JPH::Body& inBody2,
const JPH::ContactManifold& inManifold,
JPH::ContactSettings& ioSettings
) override
{
// std::cout << "A contact was added" << std::endl;
}
virtual void OnContactPersisted(
const JPH::Body& inBody1,
const JPH::Body& inBody2,
const JPH::ContactManifold& inManifold,
JPH::ContactSettings& ioSettings
) override
{
// std::cout << "A contact was persisted" << std::endl;
}
virtual void OnContactRemoved(const JPH::SubShapeIDPair& inSubShapePair) override
{
// std::cout << "A contact was removed" << std::endl;
}
};
// An example activation listener
class MyBodyActivationListener : public JPH::BodyActivationListener
{
public:
virtual void OnBodyActivated(const JPH::BodyID& inBodyID, uint64_t inBodyUserData) override
{
// std::cout << "A body got activated" << std::endl;
}
virtual void OnBodyDeactivated(const JPH::BodyID& inBodyID, uint64_t inBodyUserData) override
{
// std::cout << "A body went to sleep" << std::endl;
}
};
class Physics final : public IPhysics
{
public:
static void Register();
Physics();
~Physics();
Physics(const Physics&) = delete;
Physics(Physics&&) = delete;
Physics& operator=(const Physics&) = delete;
Physics& operator=(Physics&&) = delete;
void Optimize() override;
void Update() override;
JPH::BodyID CreateBody(JPH::BodyCreationSettings settings) override;
void AddBody(JPH::BodyID id) override;
void RemoveBody(JPH::BodyID id) override;
void DestroyBody(JPH::BodyID id) override;
void GetBodyTransform(JPH::BodyID id, std::shared_ptr<STransform> trans) override;
void SetBodyTransform(JPH::BodyID id, std::shared_ptr<STransform> trans) override;
void SetBodyPosition(JPH::BodyID id, glm::vec3 pos) override;
void SetBodyRotation(JPH::BodyID id, glm::quat rot) override;
void SetBodyVelocity(JPH::BodyID id, glm::vec3 vel) override;
void SetBodyAngularVelocity(JPH::BodyID id, glm::vec3 vel) override;
private:
JPH::PhysicsSystem m_physicsSystem;
JPH::TempAllocatorImpl m_tempAllocator;
JobSystemThreadPool m_jobSystem;
BPLayerInterfaceImpl m_broadPhaseLayerInterface;
ObjectVsBroadPhaseLayerFilterImpl m_objectVsBroadphaseLayerFilter;
ObjectLayerPairFilterImpl m_objectVsObjectLayerFilter;
MyContactListener m_contactListener;
MyBodyActivationListener m_bodyActivationListener;
float m_maxDeltaTime;
};
}; // namespace legs

View file

@ -0,0 +1,120 @@
#pragma once
#include <memory>
#include <stdexcept>
#include <legs/jolt_pch.hpp>
#include <legs/components/transform.hpp>
namespace legs
{
namespace Layers
{
static constexpr JPH::ObjectLayer NON_MOVING = 0;
static constexpr JPH::ObjectLayer MOVING = 1;
static constexpr JPH::ObjectLayer NUM_LAYERS = 2;
}; // namespace Layers
class ObjectLayerPairFilterImpl : public JPH::ObjectLayerPairFilter
{
public:
virtual bool ShouldCollide(JPH::ObjectLayer inObject1, JPH::ObjectLayer inObject2)
const override
{
switch (inObject1)
{
case Layers::NON_MOVING:
return inObject2 == Layers::MOVING;
case Layers::MOVING:
return true;
default:
JPH_ASSERT(false);
return false;
}
}
};
class ICollider
{
public:
ICollider() = default;
virtual ~ICollider() = default;
ICollider(const ICollider&) = default;
ICollider(ICollider&&) = default;
ICollider& operator=(const ICollider&) = default;
ICollider& operator=(ICollider&&) = default;
void CreateBody(std::shared_ptr<STransform> trans)
{
JPH::ShapeSettings::ShapeResult shapeResult = ShapeSettings->Create();
if (shapeResult.HasError())
{
throw std::runtime_error(std::format("Jolt: {}", shapeResult.GetError()));
}
JPH::ShapeRefC shape = shapeResult.Get();
CreationSettings = JPH::BodyCreationSettings(
shape,
JPH::RVec3(trans->position.x, trans->position.y, trans->position.z),
JPH::Quat(
trans->rotation.quaternion.x,
trans->rotation.quaternion.y,
trans->rotation.quaternion.z,
trans->rotation.quaternion.w
),
MotionType,
Layer
);
}
JPH::EMotionType MotionType;
JPH::ObjectLayer Layer;
JPH::BodyCreationSettings CreationSettings;
std::shared_ptr<JPH::ShapeSettings> ShapeSettings;
};
class BoxCollider final : public ICollider
{
public:
BoxCollider(
JPH::EMotionType motionType,
JPH::ObjectLayer layer,
std::shared_ptr<STransform> trans,
glm::vec3 size
)
{
MotionType = motionType;
Layer = layer;
ShapeSettings = std::make_shared<JPH::BoxShapeSettings>(JPH::Vec3(size.x, size.y, size.z));
CreateBody(trans);
}
~BoxCollider()
{
}
};
class SphereCollider final : public ICollider
{
public:
SphereCollider(
JPH::EMotionType motionType,
JPH::ObjectLayer layer,
std::shared_ptr<STransform> trans,
float radius
)
{
MotionType = motionType;
Layer = layer;
ShapeSettings = std::make_shared<JPH::SphereShapeSettings>(radius);
CreateBody(trans);
}
~SphereCollider()
{
}
};
}; // namespace legs

View file

@ -0,0 +1,12 @@
#pragma once
#include <glm/vec2.hpp>
namespace legs
{
struct SRect
{
glm::ivec2 size;
glm::ivec2 offset;
};
} // namespace legs

View file

@ -0,0 +1,26 @@
#pragma once
#include <glm/ext/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/vec3.hpp>
namespace legs
{
struct SRotation
{
SRotation()
{
euler = {};
quaternion = glm::identity<glm::quat>();
}
void UpdateQuaternion()
{
quaternion = glm::angleAxis(glm::radians(euler.z), glm::vec3 {0.0f, 0.0f, 1.0f})
* glm::angleAxis(glm::radians(euler.x), glm::vec3 {1.0f, 0.0f, 0.0f});
}
glm::vec3 euler;
glm::quat quaternion;
};
} // namespace legs

View file

@ -0,0 +1,43 @@
#pragma once
#include <glm/ext/matrix_float4x4.hpp>
#include <glm/ext/matrix_transform.hpp>
#include <glm/vec3.hpp>
#include <legs/components/rotation.hpp>
namespace legs
{
struct STransform
{
glm::vec3 position;
SRotation rotation;
glm::vec3 velocity;
glm::vec3 angularVelocity;
glm::mat4x4 GetModelMatrix()
{
auto model = glm::identity<glm::mat4x4>();
model = glm::translate(model, position);
model = glm::rotate(model, glm::radians(rotation.euler.x), glm::vec3 {1, 0, 0});
model = glm::rotate(model, glm::radians(rotation.euler.y), glm::vec3 {0, 1, 0});
model = glm::rotate(model, glm::radians(rotation.euler.z), glm::vec3 {0, 0, 1});
return model;
}
glm::vec3 Forward()
{
return rotation.quaternion * glm::vec3(0.0f, 1.0f, 0.0f);
}
glm::vec3 Right()
{
return rotation.quaternion * glm::vec3(1.0f, 0.0f, 0.0f);
}
glm::vec3 Up()
{
return rotation.quaternion * glm::vec3(0.0f, 0.0f, 1.0f);
}
};
} // namespace legs

View file

@ -0,0 +1,99 @@
#pragma once
#include <memory>
#include <semaphore>
#include <stop_token>
#include <thread>
#include <glm/gtc/quaternion.hpp>
#include <glm/vec3.hpp>
#include <legs/world/world.hpp>
#include <legs/isystem.hpp>
#include <legs/renderer/renderer.hpp>
#include <legs/ui/ui.hpp>
#include <legs/window/input.hpp>
#include <legs/window/window.hpp>
namespace legs
{
class Engine
{
public:
Engine();
~Engine();
int Run();
std::shared_ptr<Window> GetWindow() const
{
return m_window;
}
void AddSystem(std::shared_ptr<ISystem> system)
{
m_systems.push_back(system);
}
std::shared_ptr<Renderer> GetRenderer()
{
return m_renderer;
}
std::shared_ptr<World> GetWorld()
{
return m_world;
}
std::shared_ptr<Camera> GetCamera()
{
return m_camera;
}
void SetCamera(std::shared_ptr<Camera> camera)
{
m_camera = camera;
}
WindowInput GetFrameInput()
{
return m_frameInput;
}
WindowInput GetTickInput()
{
return m_tickInput;
}
private:
void Frame();
bool Tick();
void UpdateInput();
void TickThread(const std::stop_token token);
void RenderThread(const std::stop_token token);
std::shared_ptr<InputSettings> m_inputSettings;
std::shared_ptr<Window> m_window;
std::shared_ptr<Camera> m_camera;
std::shared_ptr<Renderer> m_renderer;
std::shared_ptr<World> m_world;
std::unique_ptr<UI> m_ui;
WindowInput m_frameInput;
WindowInput m_tickInput;
std::jthread m_tickThread;
std::jthread m_renderThread;
std::binary_semaphore m_mainTickSemaphore {0};
std::binary_semaphore m_threadTickSemaphore {0};
std::binary_semaphore m_mainFrameSemaphore {0};
std::binary_semaphore m_threadFrameSemaphore {0};
std::vector<std::shared_ptr<ISystem>> m_systems;
};
} // namespace legs

View file

@ -0,0 +1,139 @@
#pragma once
#include <algorithm>
#include <glm/ext/matrix_transform.hpp>
#include <glm/ext/quaternion_float.hpp>
#include <glm/ext/quaternion_transform.hpp>
#include <glm/gtc/constants.hpp>
#include <legs/components/transform.hpp>
#include <legs/entity/entity.hpp>
#include <legs/time.hpp>
#include <legs/window/input.hpp>
namespace legs
{
#define CAM_NEAR 0.1f
#define CAM_FAR 1000.0f
class Camera : public Entity
{
public:
Camera(int width, int height) : Entity()
{
fov = 60.0f;
near = CAM_NEAR;
far = CAM_FAR;
UpdateViewport(width, height);
UpdateMatrices();
}
virtual ~Camera() {};
void UpdateMatrices()
{
Transform->rotation.UpdateQuaternion();
view = glm::lookAt(
Transform->position,
Transform->position + Transform->Forward(),
glm::vec3(0.0f, 0.0f, 1.0f)
);
proj = glm::perspective(glm::radians(fov), aspect, near, far);
proj[1][1] *= -1; // OpenGL Y flip
}
void UpdateViewport(int width, int height)
{
aspect = static_cast<float>(width) / static_cast<float>(height);
viewport.x = static_cast<float>(width);
viewport.y = static_cast<float>(height);
viewport.z = CAM_NEAR;
viewport.w = CAM_FAR;
}
float fov;
float aspect;
float near;
float far;
glm::vec4 viewport;
glm::mat4 view;
glm::mat4 proj;
};
class NoclipCamera : public Camera
{
public:
NoclipCamera(int width, int height) : Camera(width, height)
{
MoveSpeed = 1.0f;
Sensitivity = 2.0f;
}
void HandleInput(WindowInput input)
{
if (input.mouse.x != 0)
{
Transform->rotation.euler.z -= 0.022f * 3.14f * static_cast<float>(input.mouse.x);
while (Transform->rotation.euler.z < -180.0f)
{
Transform->rotation.euler.z += 360.0f;
}
while (Transform->rotation.euler.z > 180.0f)
{
Transform->rotation.euler.z -= 360.0f;
}
}
if (input.mouse.y != 0)
{
Transform->rotation.euler.x -= 0.022f * 3.14f * static_cast<float>(input.mouse.y);
Transform->rotation.euler.x = std::clamp(Transform->rotation.euler.x, -89.0f, 89.0f);
}
if (input.scroll.y > 0)
{
MoveSpeed *= 2;
}
else if (input.scroll.y < 0)
{
MoveSpeed /= 2;
}
if (input.HasKey(Key::KEY_MOVE_FORWARD))
{
Transform->position +=
Transform->Forward() * MoveSpeed * static_cast<float>(Time::DeltaFrame);
}
if (input.HasKey(Key::KEY_MOVE_BACK))
{
Transform->position -=
Transform->Forward() * MoveSpeed * static_cast<float>(Time::DeltaFrame);
}
if (input.HasKey(Key::KEY_MOVE_RIGHT))
{
Transform->position +=
Transform->Right() * MoveSpeed * static_cast<float>(Time::DeltaFrame);
}
if (input.HasKey(Key::KEY_MOVE_LEFT))
{
Transform->position -=
Transform->Right() * MoveSpeed * static_cast<float>(Time::DeltaFrame);
}
if (input.HasKey(Key::KEY_MOVE_UP))
{
Transform->position +=
glm::vec3(0, 0, 1) * MoveSpeed * static_cast<float>(Time::DeltaFrame);
}
if (input.HasKey(Key::KEY_MOVE_DOWN))
{
Transform->position -=
glm::vec3(0, 0, 1) * MoveSpeed * static_cast<float>(Time::DeltaFrame);
}
UpdateMatrices();
}
float MoveSpeed;
float Sensitivity;
};
} // namespace legs

View file

@ -0,0 +1,75 @@
#pragma once
#include <memory>
#include <string>
#include <legs/components/transform.hpp>
namespace legs
{
class Entity
{
public:
Entity() : Name(""), Transform(std::make_shared<STransform>()) {};
virtual ~Entity() = default;
Entity(const Entity&) = delete;
Entity(Entity&&) = delete;
Entity& operator=(const Entity&) = delete;
Entity& operator=(Entity&&) = delete;
virtual void OnSpawn() {};
virtual void OnDestroy() {};
virtual void OnFrame() {};
virtual void OnTick() {};
virtual void SetPosition(glm::vec3 pos)
{
Transform->position = pos;
}
virtual void SetRotation(glm::quat rot)
{
Transform->rotation.quaternion = rot;
}
virtual void SetVelocity(glm::vec3 vel)
{
Transform->velocity = vel;
}
virtual void SetAngularVelocity(glm::vec3 vel)
{
Transform->angularVelocity = vel;
}
virtual std::shared_ptr<STransform> GetTransform()
{
return Transform;
}
virtual glm::vec3 GetPosition()
{
return Transform->position;
}
virtual glm::quat GetRotation()
{
return Transform->rotation.quaternion;
}
virtual glm::vec3 GetVelocity()
{
return Transform->velocity;
}
virtual glm::vec3 GetAngularVelocity()
{
return Transform->angularVelocity;
}
protected:
std::string Name;
std::shared_ptr<STransform> Transform;
};
}; // namespace legs

View file

@ -0,0 +1,77 @@
#pragma once
#include <memory>
#include <string>
#include <legs/entity/entity.hpp>
#include <legs/renderer/buffer.hpp>
#include <legs/renderer/renderer.hpp>
namespace legs
{
class MeshEntity : public Entity
{
public:
MeshEntity() : Entity(), m_pipeline(RenderPipeline::INVALID)
{
}
virtual ~MeshEntity() = default;
MeshEntity(const MeshEntity&) = delete;
MeshEntity(MeshEntity&&) = delete;
MeshEntity& operator=(const MeshEntity&) = delete;
MeshEntity& operator=(MeshEntity&&) = delete;
virtual void OnSpawn() override
{
Entity::OnSpawn();
}
virtual void OnDestroy() override
{
Entity::OnDestroy();
}
virtual void OnFrame() override
{
Entity::OnFrame();
}
virtual void OnTick() override
{
Entity::OnTick();
}
virtual void SetBuffers(
std::shared_ptr<Buffer> vertexBuffer,
std::shared_ptr<Buffer> indexBuffer
)
{
m_vertexBuffer = vertexBuffer;
m_indexBuffer = indexBuffer;
}
virtual void Render(std::shared_ptr<Renderer> renderer)
{
if (m_pipeline == RenderPipeline::INVALID)
{
return;
}
// TODO: Transform matrices
renderer->BindPipeline(m_pipeline);
renderer->DrawWithBuffers(m_vertexBuffer, m_indexBuffer);
}
virtual void SetPipeline(RenderPipeline pipeline)
{
m_pipeline = pipeline;
}
protected:
RenderPipeline m_pipeline;
std::shared_ptr<Buffer> m_vertexBuffer;
std::shared_ptr<Buffer> m_indexBuffer;
};
}; // namespace legs

View file

@ -0,0 +1,85 @@
#pragma once
#include <memory>
#include <legs/collider.hpp>
#include <legs/iphysics.hpp>
#include <legs/entity/mesh_entity.hpp>
#include <legs/entry.hpp>
namespace legs
{
class PhysicsEntity : public MeshEntity
{
public:
PhysicsEntity() : MeshEntity()
{
}
virtual ~PhysicsEntity() = default;
PhysicsEntity(const PhysicsEntity&) = delete;
PhysicsEntity(PhysicsEntity&&) = delete;
PhysicsEntity& operator=(const PhysicsEntity&) = delete;
PhysicsEntity& operator=(PhysicsEntity&&) = delete;
virtual void OnSpawn() override
{
MeshEntity::OnSpawn();
m_joltBody = g_engine->GetWorld()->GetPhysics()->CreateBody(m_collider.CreationSettings);
g_engine->GetWorld()->GetPhysics()->AddBody(m_joltBody);
}
virtual void OnDestroy() override
{
MeshEntity::OnDestroy();
g_engine->GetWorld()->GetPhysics()->RemoveBody(m_joltBody);
g_engine->GetWorld()->GetPhysics()->DestroyBody(m_joltBody);
}
virtual void OnFrame() override
{
MeshEntity::OnFrame();
}
virtual void OnTick() override
{
MeshEntity::OnTick();
g_engine->GetWorld()->GetPhysics()->GetBodyTransform(m_joltBody, Transform);
}
virtual void SetPosition(glm::vec3 pos) override
{
MeshEntity::SetPosition(pos);
g_engine->GetWorld()->GetPhysics()->SetBodyPosition(m_joltBody, pos);
}
virtual void SetRotation(glm::quat rot) override
{
MeshEntity::SetRotation(rot);
g_engine->GetWorld()->GetPhysics()->SetBodyRotation(m_joltBody, rot);
}
virtual void SetVelocity(glm::vec3 vel) override
{
MeshEntity::SetVelocity(vel);
g_engine->GetWorld()->GetPhysics()->SetBodyVelocity(m_joltBody, vel);
}
virtual void SetAngularVelocity(glm::vec3 vel) override
{
MeshEntity::SetAngularVelocity(vel);
g_engine->GetWorld()->GetPhysics()->SetBodyAngularVelocity(m_joltBody, vel);
}
virtual void SetCollider(ICollider collider)
{
m_collider = collider;
}
protected:
JPH::BodyID m_joltBody;
ICollider m_collider;
};
}; // namespace legs

View file

@ -0,0 +1,59 @@
#pragma once
#include <memory>
#include <legs/entity/camera.hpp>
#include <legs/entity/mesh_entity.hpp>
#include <legs/geometry/icosphere.hpp>
#include <legs/renderer/buffer.hpp>
#include <legs/renderer/mesh_data.hpp>
#include <legs/renderer/renderer.hpp>
namespace legs
{
class Sky : public MeshEntity
{
public:
Sky(std::shared_ptr<Renderer> renderer) : MeshEntity()
{
auto icosphere = SIcosphere({}, CAM_FAR * 0.95f, 1, true);
std::vector<Vertex_P> vertices;
vertices.reserve(icosphere.positions.size());
for (unsigned int i = 0; i < icosphere.positions.size(); i++)
{
vertices.push_back({icosphere.positions[i]});
}
renderer->CreateBuffer(
m_vertexBuffer,
VertexBuffer,
vertices.data(),
sizeof(Vertex_P),
static_cast<uint32_t>(vertices.size())
);
renderer->CreateBuffer(
m_indexBuffer,
IndexBuffer,
icosphere.indices.data(),
sizeof(Index),
static_cast<uint32_t>(icosphere.indices.size())
);
m_pipeline = RenderPipeline::SKY;
SunDirection = glm::vec3(0, 0, 1.0f);
SunColor = glm::vec3(0.5f, 0.5f, 0.5f);
}
void Render(std::shared_ptr<Renderer> renderer) override
{
renderer->GetUBO()->sunDir = SunDirection;
renderer->GetUBO()->sunColor = SunColor;
MeshEntity::Render(renderer);
}
glm::vec3 SunDirection;
glm::vec3 SunColor;
};
} // namespace legs

74
src/public/legs/entry.hpp Normal file
View file

@ -0,0 +1,74 @@
#pragma once
#include <cstring>
#include <exception>
#include <legs/engine.hpp>
#include <legs/log.hpp>
namespace legs
{
extern std::shared_ptr<Engine> g_engine;
static bool HasLaunchArg(const char* name, const char* value, int argc, char** argv)
{
for (int i = 0; i < argc; i++)
{
if (std::strcmp(name, argv[i]) == 0)
{
if (value == nullptr)
{
return true;
}
if (i < argc - 1 && std::strcmp(value, argv[i + 1]) == 0)
{
return true;
}
}
}
return false;
}
static int LEGS_Init(int /*argc*/, char** /*argv*/)
{
try
{
g_engine = std::make_shared<legs::Engine>();
return 0;
}
catch (std::exception& ex)
{
LOG_FATAL("Unhandled exception: {}", ex.what());
legs::Log::Flush();
if (g_engine != nullptr)
{
g_engine.reset();
}
return -1;
}
}
static int LEGS_Run()
{
try
{
auto code = g_engine->Run();
g_engine.reset();
return code;
}
catch (std::exception& ex)
{
LOG_FATAL("Unhandled exception: {}", ex.what());
legs::Log::Flush();
if (g_engine != nullptr)
{
g_engine.reset();
}
return -1;
}
}
}; // namespace legs

View file

@ -0,0 +1,195 @@
#pragma once
#include <cmath>
#include <glm/geometric.hpp>
#include <map>
#include <stdexcept>
#include <utility>
#include <vector>
#include <glm/vec2.hpp>
#include <glm/vec3.hpp>
#include <legs/log.hpp>
#include <legs/renderer/mesh_data.hpp>
namespace legs
{
static const glm::vec2 baseSize = {0.525731112119133606f, 0.850650808352039932f};
static const std::array<glm::vec3, 12> basePositions = {
glm::vec3 {-baseSize.x, 0.0f, baseSize.y},
glm::vec3 { baseSize.x, 0.0f, baseSize.y},
glm::vec3 {-baseSize.x, 0.0f, -baseSize.y},
glm::vec3 { baseSize.x, 0.0f, -baseSize.y},
glm::vec3 { 0.0f, baseSize.y, baseSize.x},
glm::vec3 { 0.0f, baseSize.y, -baseSize.x},
glm::vec3 { 0.0f, -baseSize.y, baseSize.x},
glm::vec3 { 0.0f, -baseSize.y, -baseSize.x},
glm::vec3 { baseSize.y, baseSize.x, 0.0f},
glm::vec3 {-baseSize.y, baseSize.x, 0.0f},
glm::vec3 { baseSize.y, -baseSize.x, 0.0f},
glm::vec3 {-baseSize.y, -baseSize.x, 0.0f},
};
static const std::array<unsigned int, 60> baseIndices = {
// clang-format off
0, 1, 4,
0, 4, 9,
9, 4, 5,
4, 8, 5,
4, 1, 8,
8, 1, 10,
8, 10, 3,
5, 8, 3,
5, 3, 2,
2, 3, 7,
7, 3, 10,
7, 10, 6,
7, 6, 11,
11, 6, 0,
0, 6, 1,
6, 10, 1,
9, 11, 0,
9, 2, 11,
9, 5, 2,
7, 11, 2
// clang-format on
};
using EdgeMap = std::map<std::pair<Index, Index>, unsigned int>;
struct SIcosphere
{
std::vector<glm::vec3> positions;
std::vector<glm::vec3> normals;
std::vector<Index> indices;
SIcosphere(glm::vec3 origin, float radius, unsigned int subdivisions, bool invert = false)
{
// Don't be silly...
// 10k verts should be enough for anything
const unsigned int maxDiv = 5;
if (subdivisions > maxDiv)
{
LOG_WARN("Tried to create icosphere with subdiv {}, clamping", subdivisions);
subdivisions = maxDiv;
}
auto numIndices = static_cast<size_t>(60 * std::pow(4, subdivisions));
auto numVerts = static_cast<size_t>(12 + 10 * (std::pow(4, subdivisions) - 1));
positions.reserve(numVerts);
indices.reserve(60);
for (const auto& p : basePositions)
{
positions.push_back(p);
}
for (const auto& i : baseIndices)
{
indices.push_back(i);
}
for (unsigned int i = 0; i < subdivisions; i++)
{
Subdivide();
}
if (numVerts != positions.size())
{
LOG_ERROR(
"Generated {} vertices for icosphere {}, expected {}",
positions.size(),
subdivisions,
numVerts
);
numVerts = positions.size();
}
if (numIndices != indices.size())
{
LOG_ERROR(
"Generated {} indices for icosphere, expected {}",
indices.size(),
numIndices
);
numIndices = indices.size();
}
normals.resize(numVerts);
for (unsigned int i = 0; i < numVerts; i++)
{
normals[i] = invert ? -positions[i] : positions[i];
positions[i] = positions[i] * radius + origin;
}
if (invert)
{
for (unsigned int i = 0; i < numIndices; i += 3)
{
std::swap(indices[i], indices[i + 1]);
}
}
}
void Subdivide()
{
EdgeMap edgeMap;
std::vector<Index> newIndices;
newIndices.reserve(indices.size() * 4);
for (unsigned int i = 0; i < indices.size(); i += 3)
{
unsigned int mid[3];
for (unsigned int edge = 0; edge < 3; edge++)
{
const auto first = i + edge;
const auto second = i + ((edge + 1) % 3);
mid[edge] = EdgeVertex(edgeMap, indices[first], indices[second]);
}
newIndices.push_back(indices[i]);
newIndices.push_back(mid[0]);
newIndices.push_back(mid[2]);
newIndices.push_back(indices[i + 1]);
newIndices.push_back(mid[1]);
newIndices.push_back(mid[0]);
newIndices.push_back(indices[i + 2]);
newIndices.push_back(mid[2]);
newIndices.push_back(mid[1]);
newIndices.push_back(mid[0]);
newIndices.push_back(mid[1]);
newIndices.push_back(mid[2]);
}
indices = newIndices;
}
unsigned int EdgeVertex(EdgeMap& edgeMap, Index first, Index second)
{
EdgeMap::key_type key(first, second);
if (key.first > key.second)
{
std::swap(key.first, key.second);
}
const auto inserted = edgeMap.insert({key, positions.size()});
if (inserted.second)
{
auto& edge0 = positions[first];
auto& edge1 = positions[second];
auto pos = glm::normalize(edge0 + edge1);
positions.push_back(pos);
}
return inserted.first->second;
}
};
}; // namespace legs

View file

@ -0,0 +1,31 @@
#pragma once
#include <glm/vec3.hpp>
#include <legs/renderer/mesh_data.hpp>
namespace legs
{
static const std::array<glm::vec3, 4> planeBaseVertices = {
glm::vec3 {-0.5f, -0.5f, 0.0f},
glm::vec3 { 0.5f, -0.5f, 0.0f},
glm::vec3 { 0.5f, 0.5f, 0.0f},
glm::vec3 {-0.5f, 0.5f, 0.0f},
};
static const std::array<Index, 6> planeBaseIndices = {0, 1, 2, 2, 3, 0};
struct SPlane
{
std::array<glm::vec3, 4> vertices;
std::array<Index, 6> indices;
SPlane(glm::vec3 position, float scale = 1.0f)
{
indices = planeBaseIndices;
for (unsigned int i = 0; i < 4; i++)
{
vertices[i] = position + planeBaseVertices[i] * scale;
}
}
};
}; // namespace legs

View file

@ -0,0 +1,38 @@
#pragma once
#include <memory>
#include <legs/jolt_pch.hpp>
#include <legs/components/transform.hpp>
namespace legs
{
class IPhysics
{
public:
IPhysics() = default;
virtual ~IPhysics() = default;
IPhysics(const IPhysics&) = delete;
IPhysics(IPhysics&&) = delete;
IPhysics& operator=(const IPhysics&) = delete;
IPhysics& operator=(IPhysics&&) = delete;
virtual void Update() = 0;
virtual void Optimize() = 0;
virtual JPH::BodyID CreateBody(JPH::BodyCreationSettings settings) = 0;
virtual void AddBody(JPH::BodyID id) = 0;
virtual void RemoveBody(JPH::BodyID id) = 0;
virtual void DestroyBody(JPH::BodyID id) = 0;
virtual void GetBodyTransform(JPH::BodyID id, std::shared_ptr<STransform> trans) = 0;
virtual void SetBodyTransform(JPH::BodyID id, std::shared_ptr<STransform> trans) = 0;
virtual void SetBodyPosition(JPH::BodyID id, glm::vec3 pos) = 0;
virtual void SetBodyRotation(JPH::BodyID id, glm::quat rot) = 0;
virtual void SetBodyVelocity(JPH::BodyID id, glm::vec3 vel) = 0;
virtual void SetBodyAngularVelocity(JPH::BodyID id, glm::vec3 vel) = 0;
};
}; // namespace legs

View file

@ -0,0 +1,20 @@
#pragma once
namespace legs
{
class ISystem
{
public:
ISystem() = default;
virtual ~ISystem() = default;
ISystem(const ISystem&) = delete;
ISystem(ISystem&&) = delete;
ISystem& operator=(const ISystem&) = delete;
ISystem& operator=(ISystem&&) = delete;
virtual void OnLevelLoad() {};
virtual void OnFrame() {};
virtual void OnTick() {};
};
}; // namespace legs

View file

@ -0,0 +1,18 @@
#pragma once
#include <Jolt/Jolt.h>
#include <Jolt/Core/Factory.h>
#include <Jolt/Core/FixedSizeFreeList.h>
#include <Jolt/Core/JobSystem.h>
#include <Jolt/Core/Semaphore.h>
#include <Jolt/Core/TempAllocator.h>
#include <Jolt/Physics/Body/Body.h>
#include <Jolt/Physics/Body/BodyActivationListener.h>
#include <Jolt/Physics/Body/BodyCreationSettings.h>
#include <Jolt/Physics/Collision/ObjectLayer.h>
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
#include <Jolt/Physics/PhysicsSettings.h>
#include <Jolt/Physics/PhysicsSystem.h>
#include <Jolt/RegisterTypes.h>

100
src/public/legs/log.hpp Normal file
View file

@ -0,0 +1,100 @@
#pragma once
#include <cstdio>
#include <cstring>
#include <format>
#include <iostream>
#include <mutex>
#include <ostream>
#include <legs/time.hpp>
namespace legs
{
enum LogLevel
{
Debug,
Info,
Warn,
Error,
Fatal,
MAX,
};
class Log
{
public:
static void SetLogLevel(const LogLevel level)
{
m_logLevel = level;
}
template<typename... Args>
static void Print(
const char* file,
const unsigned int line,
const char* func,
const LogLevel level,
const char* fmt,
Args&&... args
)
{
if (level < m_logLevel)
{
return;
}
auto vargs = std::vformat(fmt, std::make_format_args(args...));
auto message = std::format(
"[{:.3f}]" // Time
"[{}]" // Severity
"[{}:{}@{}()] " // Location
"{}", // Message
Time::Now(),
m_severityStrings[static_cast<int>(level)],
file,
line,
func,
vargs
);
const std::scoped_lock lock {m_logMutex};
std::cout << message << '\n';
#if !NDEBUG
Flush();
#endif
}
static void Flush()
{
std::flush(std::cout);
}
private:
static inline LogLevel m_logLevel;
static constexpr const char* m_severityStrings[static_cast<int>(LogLevel::MAX)] = {
"DEBUG",
"INFO",
"WARN",
"ERROR",
"FATAL",
};
static inline std::mutex m_logMutex;
};
#define __FILENAME__ (strrchr("/" __FILE__, '/') + 1)
#define _LOG(L, F, ...) legs::Log::Print(__FILENAME__, __LINE__, __func__, L, F, ##__VA_ARGS__)
#define LOG_DEBUG(F, ...) _LOG(legs::LogLevel::Debug, F, ##__VA_ARGS__)
#define LOG_INFO(F, ...) _LOG(legs::LogLevel::Info, F, ##__VA_ARGS__)
#define LOG_WARN(F, ...) _LOG(legs::LogLevel::Warn, F, ##__VA_ARGS__)
#define LOG_ERROR(F, ...) _LOG(legs::LogLevel::Error, F, ##__VA_ARGS__)
#define LOG_FATAL(F, ...) _LOG(legs::LogLevel::Fatal, F, ##__VA_ARGS__)
} // namespace legs

View file

@ -0,0 +1,23 @@
#pragma once
#include <sys/resource.h>
#include <sys/time.h>
namespace legs
{
class Memory
{
public:
// Get resident memory usage in KB.
static long int GetUsage()
{
rusage ru = {};
if (getrusage(RUSAGE_SELF, &ru) == 0)
{
return ru.ru_maxrss;
}
return -1;
}
};
}; // namespace legs

View file

@ -0,0 +1,120 @@
#pragma once
#include <cstring>
#include <memory>
#include <legs/renderer/vma_usage.hpp>
namespace legs
{
enum BufferType
{
VertexBuffer,
IndexBuffer,
UniformBuffer,
};
enum BufferLocation
{
HostBuffer,
DeviceBuffer,
};
class Buffer
{
public:
Buffer() = delete;
Buffer(
BufferType bufferType,
BufferLocation bufferLocation,
uint32_t elementSize,
uint32_t elementCount
);
~Buffer();
Buffer(const Buffer&) = delete;
Buffer(Buffer&&) = delete;
Buffer& operator=(const Buffer&) = delete;
Buffer& operator=(Buffer&&) = delete;
template<class IT>
requires std::contiguous_iterator<IT>
void Write(const IT it, uint32_t length)
{
if (m_bufferLocation != HostBuffer)
{
throw std::runtime_error("Tried mapping a non-host buffer");
}
size_t size = length * m_elementSize;
if (size > m_size)
{
throw std::runtime_error("Tried to write more data than allocated");
}
Write(static_cast<void*>(it), size);
m_elementCount = length;
}
void Write(void* data, size_t size);
void CopyToDevice(void* commandBuffer, std::shared_ptr<Buffer> deviceBuffer);
void Clear();
void Bind(void* commandBuffer);
void Draw(void* commandBuffer);
void Draw(void* commandBuffer, uint32_t indexOffset, uint32_t indexCount, int32_t vertexOffset);
void Map(void** data);
void Unmap();
VkBuffer GetVkBuffer() const
{
return m_vkBuffer;
}
void SetElementCount(uint32_t count)
{
m_elementCount = count;
}
uint32_t GetElementCount() const
{
return m_elementCount;
}
constexpr BufferType GetType() const
{
return m_bufferType;
}
BufferLocation GetLocation() const
{
return m_bufferLocation;
}
size_t GetSize() const
{
return m_size;
}
size_t GetElementSize()
{
return m_elementSize;
}
private:
VkBuffer m_vkBuffer;
VmaAllocation m_vmaAllocation;
BufferType m_bufferType;
BufferLocation m_bufferLocation;
uint32_t m_elementSize;
uint32_t m_elementCount;
size_t m_size;
bool m_isMapped = false;
};
} // namespace legs

View file

@ -0,0 +1,159 @@
#pragma once
#include <stdexcept>
#include <imgui_impl_vulkan.h>
#include <vulkan/vulkan_core.h>
namespace legs
{
#define VK_CHECK(RESULT, MSG) \
if (RESULT != VK_SUCCESS) \
{ \
throw std::runtime_error(MSG); \
}
constexpr static void ImGuiVkCheck(VkResult result)
{
VK_CHECK(result, "ImGuiVkCheck failed");
}
// Hold color format so it doesn't get dropped from stack
struct ImGuiCreationInfo
{
VkFormat colorFormat;
VkPipelineRenderingCreateInfoKHR pipelineCreateInfo;
ImGui_ImplVulkan_InitInfo imGuiInfo;
};
constexpr static VkPipelineStageFlags GetPipelineStageFlags(const VkImageLayout imageLayout)
{
switch (imageLayout)
{
case VK_IMAGE_LAYOUT_UNDEFINED:
return VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
case VK_IMAGE_LAYOUT_PREINITIALIZED:
return VK_PIPELINE_STAGE_HOST_BIT;
case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
return VK_PIPELINE_STAGE_TRANSFER_BIT;
case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
return VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL:
return VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT
| VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
case VK_IMAGE_LAYOUT_FRAGMENT_SHADING_RATE_ATTACHMENT_OPTIMAL_KHR:
return VK_PIPELINE_STAGE_FRAGMENT_SHADING_RATE_ATTACHMENT_BIT_KHR;
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
return VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
case VK_IMAGE_LAYOUT_PRESENT_SRC_KHR:
return VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
default:
throw std::runtime_error("Unhandled VkImageLayout to VkPipelineStageFlags conversion");
}
}
constexpr static VkAccessFlags GetAccessFlags(const VkImageLayout imageLayout)
{
switch (imageLayout)
{
case VK_IMAGE_LAYOUT_UNDEFINED:
case VK_IMAGE_LAYOUT_PRESENT_SRC_KHR:
return 0;
case VK_IMAGE_LAYOUT_PREINITIALIZED:
return VK_ACCESS_HOST_WRITE_BIT;
case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
return VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL:
return VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT
| VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
case VK_IMAGE_LAYOUT_FRAGMENT_SHADING_RATE_ATTACHMENT_OPTIMAL_KHR:
return VK_ACCESS_FRAGMENT_SHADING_RATE_ATTACHMENT_READ_BIT_KHR;
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
return VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_INPUT_ATTACHMENT_READ_BIT;
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
return VK_ACCESS_TRANSFER_READ_BIT;
case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
return VK_ACCESS_TRANSFER_WRITE_BIT;
default:
throw std::runtime_error("Unhandled VkImageLayout to VkAccessFlags conversion");
}
}
constexpr static void TransitionImageLayout(
VkCommandBuffer commandBuffer,
VkImage image,
VkImageLayout oldLayout,
VkImageLayout newLayout,
const VkImageSubresourceRange& subresourceRange,
const VkPipelineStageFlags srcStageMask,
const VkPipelineStageFlags dstStageMask,
const VkAccessFlags srcAccessMask,
const VkAccessFlags dstAccessMask
)
{
VkImageMemoryBarrier barrier {};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.srcAccessMask = srcAccessMask;
barrier.dstAccessMask = dstAccessMask;
barrier.oldLayout = oldLayout;
barrier.newLayout = newLayout;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = image;
barrier.subresourceRange = subresourceRange;
vkCmdPipelineBarrier(
commandBuffer,
srcStageMask,
dstStageMask,
0,
0,
nullptr,
0,
nullptr,
1,
&barrier
);
}
constexpr static void TransitionImageLayout(
VkCommandBuffer commandBuffer,
VkImage image,
VkImageLayout oldLayout,
VkImageLayout newLayout,
const VkImageSubresourceRange& subresourceRange
)
{
const VkPipelineStageFlags srcStageMask = GetPipelineStageFlags(oldLayout);
const VkPipelineStageFlags dstStageMask = GetPipelineStageFlags(newLayout);
const VkAccessFlags srcAccessMask = GetAccessFlags(oldLayout);
const VkAccessFlags dstAccessMask = GetAccessFlags(newLayout);
TransitionImageLayout(
commandBuffer,
image,
oldLayout,
newLayout,
subresourceRange,
srcStageMask,
dstStageMask,
srcAccessMask,
dstAccessMask
);
}
constexpr static void TransitionImageLayout(
VkCommandBuffer commandBuffer,
VkImage image,
VkImageLayout oldLayout,
VkImageLayout newLayout
)
{
VkImageSubresourceRange subRange {};
subRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
subRange.baseMipLevel = 0;
subRange.levelCount = 1;
subRange.baseArrayLayer = 0;
subRange.layerCount = 1;
TransitionImageLayout(commandBuffer, image, oldLayout, newLayout, subRange);
}
} // namespace legs

View file

@ -0,0 +1,45 @@
#pragma once
#include <vulkan/vulkan_core.h>
#include <legs/renderer/buffer.hpp>
#include <legs/renderer/device.hpp>
#include <legs/renderer/mesh_data.hpp>
#include <legs/renderer/ubo.hpp>
namespace legs
{
class DescriptorSet
{
public:
DescriptorSet() = delete;
DescriptorSet(const Device& device, std::vector<std::shared_ptr<Buffer>> uboBuffers);
~DescriptorSet();
DescriptorSet(const DescriptorSet&) = delete;
DescriptorSet(DescriptorSet&&) = default;
DescriptorSet& operator=(const DescriptorSet&) = delete;
DescriptorSet& operator=(DescriptorSet&&) = delete;
void UpdateUBO(uint32_t frameIndex, const std::shared_ptr<UniformBufferObject> ubo);
void Bind(
VkCommandBuffer commandBuffer,
VkPipelineBindPoint bindPoint,
VkPipelineLayout pipelineLayout,
uint32_t frameIndex
);
std::vector<VkDescriptorSetLayout> GetLayouts() const
{
return m_vkLayouts;
}
private:
const Device& m_device;
std::vector<VkDescriptorSetLayout> m_vkLayouts;
std::vector<VkDescriptorSet> m_vkSets;
std::vector<std::shared_ptr<Buffer>> m_uniformBuffers;
std::vector<void*> m_ubosMappedMemory;
};
} // namespace legs

View file

@ -0,0 +1,203 @@
#pragma once
#include <optional>
#include <legs/components/rect.hpp>
#include <legs/renderer/instance.hpp>
#include <legs/renderer/vma_usage.hpp>
namespace legs
{
struct QueueFamilyIndices
{
std::optional<uint32_t> graphicsFamily;
std::optional<uint32_t> presentFamily;
bool IsComplete() const
{
return graphicsFamily.has_value() && presentFamily.has_value();
}
};
struct SwapchainSupportDetails
{
VkSurfaceCapabilitiesKHR capabilities;
std::vector<VkSurfaceFormatKHR> formats;
std::vector<VkPresentModeKHR> presentModes;
};
class Device
{
public:
Device(const Instance& instance, uint32_t maxFramesInFlight);
~Device();
Device(const Device&) = delete;
Device(Device&&) = delete;
Device& operator=(const Device&) = delete;
Device& operator=(Device&&) = delete;
void Begin();
void ResetViewport();
void SetViewport(SRect rect);
void Submit();
void Present();
void WaitForGraphicsIdle();
VkCommandBuffer GetTemporaryCommandBuffer();
void SubmitTemporaryCommandBuffer(VkCommandBuffer commandBuffer);
VkCommandBuffer GetCommandBuffer() const
{
return m_vkCommandBuffers[m_currentFrame];
}
uint32_t GetCurrentFrame() const
{
return m_currentFrame;
}
void ResizeFramebuffer()
{
m_frameBufferResized = true;
}
VkPhysicalDevice GetVkPhysicalDevice() const
{
return m_vkPhysicalDevice;
}
VkDevice GetVkDevice() const
{
return m_vkDevice;
}
constexpr VkExtent2D GetSwapchainExtent() const
{
return m_vkSwapchainExtent;
}
constexpr float GetSwapchainAspect() const
{
const auto extent = GetSwapchainExtent();
return static_cast<float>(extent.width) / static_cast<float>(extent.height);
}
VkFormat GetSwapchainImageFormat() const
{
return m_vkSwapchainImageFormat;
}
VkFormat GetDepthFormat() const
{
return VK_FORMAT_D32_SFLOAT;
}
VkDescriptorPool GetUboDescriptorPool() const
{
return m_vkUboDescriptorPool;
}
VkDescriptorPool GetImGuiDescriptorPool() const
{
return m_vkImGuiDescriptorPool;
}
uint32_t GetGraphicsQueueIndex() const
{
return m_vkGraphicsQueueIndex;
}
VkQueue GetGraphicsQueue() const
{
return m_vkGraphicsQueue;
}
private:
void RecreateSwapchain();
void DestroySwapchain();
void PickPhysicalDevice();
bool IsDeviceSuitable(const VkPhysicalDevice device);
QueueFamilyIndices FindQueueFamilies(const VkPhysicalDevice device);
bool CheckDeviceExtensionSupport(const VkPhysicalDevice device);
SwapchainSupportDetails QuerySwapchainSupport(const VkPhysicalDevice device);
constexpr VkSurfaceFormatKHR ChooseSwapSurfaceFormat(
const std::vector<VkSurfaceFormatKHR>& available
);
constexpr VkPresentModeKHR ChooseSwapPresentMode(const std::vector<VkPresentModeKHR>& available
);
constexpr VkExtent2D ChooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities);
void CreateSwapchain();
void CreateImage(
VkImage* image,
VmaAllocation* imageAllocation,
VkImageType imageType,
VkFormat format,
VkImageUsageFlags usage,
uint32_t width,
uint32_t height
);
void CreateImageView(
VkImage image,
VkImageView* imageView,
VkImageViewType viewType,
VkFormat format,
VkImageAspectFlags aspect
);
void CreateImageViews();
void CreateLogicalDevice();
void CreateCommandPool();
void CreateCommandBuffers();
void CreateSyncObjects();
void CreateDescriptorPools();
const Instance& m_instance;
VkPhysicalDevice m_vkPhysicalDevice;
VkDevice m_vkDevice;
uint32_t m_vkGraphicsQueueIndex;
uint32_t m_vkPresentQueueIndex;
VkQueue m_vkGraphicsQueue;
VkQueue m_vkPresentQueue;
VkSwapchainKHR m_vkSwapchain;
std::vector<VkImage> m_vkSwapchainImages;
VkFormat m_vkSwapchainImageFormat;
VkExtent2D m_vkSwapchainExtent;
std::vector<VkImageView> m_vkSwapchainImageViews;
VkImage m_vkDepthImage;
VmaAllocation m_vmaDepthAllocation;
VkImageView m_vkDepthImageView;
VkCommandPool m_vkCommandPool;
std::vector<VkCommandBuffer> m_vkCommandBuffers;
std::vector<VkSemaphore> m_vkImageSemaphores;
std::vector<VkSemaphore> m_vkRenderSemaphores;
std::vector<VkFence> m_vkInFlightFences;
VkDescriptorPool m_vkUboDescriptorPool;
VkDescriptorPool m_vkImGuiDescriptorPool;
uint32_t m_currentImageIndex;
uint32_t m_currentFrame = 0;
const uint32_t m_maxFramesInFlight;
bool m_frameBufferResized = false;
const std::vector<const char*> m_requiredExtensions = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME,
VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME,
};
};
} // namespace legs

View file

@ -0,0 +1,59 @@
#pragma once
#include <memory>
#include <vector>
#include <legs/window/window.hpp>
namespace legs
{
class Instance
{
public:
Instance(std::shared_ptr<Window> window);
~Instance();
Instance(const Instance&) = delete;
Instance(Instance&&) = delete;
Instance& operator=(const Instance&) = delete;
Instance& operator=(Instance&&) = delete;
VkInstance GetVkInstance() const
{
return m_vkInstance;
}
VkSurfaceKHR GetSurface() const
{
return m_vkSurface;
}
void GetFramebufferSize(int* width, int* height) const
{
m_window->GetFramebufferSize(width, height);
}
void SetWindow(std::shared_ptr<Window> window);
private:
bool ValidationLayersSupported();
std::vector<const char*> GetRequiredExtensions();
void constexpr PopulateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo);
void SetupDebugMessenger();
void CreateSurface();
std::shared_ptr<Window> m_window;
VkInstance m_vkInstance;
VkDebugUtilsMessengerEXT m_vkDebugMessenger;
VkSurfaceKHR m_vkSurface;
const std::vector<const char*> m_validationLayers = {"VK_LAYER_KHRONOS_validation"};
#ifdef NDEBUG
constexpr static bool m_enableValidationLayers = false;
#else
constexpr static bool m_enableValidationLayers = true;
#endif
};
} // namespace legs

View file

@ -0,0 +1,110 @@
#pragma once
#include <memory>
#include <type_traits>
#include <glm/vec3.hpp>
#include <legs/renderer/common.hpp>
namespace legs
{
struct Vertex_P
{
glm::vec3 position;
};
struct Vertex_P_C
{
glm::vec3 position;
glm::vec3 color;
};
struct Vertex_P_N_C
{
glm::vec3 position;
glm::vec3 normal;
glm::vec3 color;
};
// For fullscreen triangle
struct VertexEmpty
{
};
typedef uint32_t Index;
template<typename T>
consteval VkVertexInputBindingDescription GetBindingDescription()
{
VkVertexInputBindingDescription desc {};
desc.binding = 0;
desc.stride = sizeof(T);
desc.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
return desc;
}
template<typename T>
constexpr std::vector<VkVertexInputAttributeDescription> GetAttributeDescriptions()
{
std::vector<VkVertexInputAttributeDescription> desc {};
// TODO: reflection
if constexpr (std::is_same_v<T, Vertex_P>)
{
desc.push_back({
.location = 0,
.binding = 0,
.format = VK_FORMAT_R32G32B32_SFLOAT,
.offset = offsetof(T, position),
});
}
else if constexpr (std::is_same_v<T, Vertex_P_C>)
{
desc.push_back({
.location = 0,
.binding = 0,
.format = VK_FORMAT_R32G32B32_SFLOAT,
.offset = offsetof(T, position),
});
desc.push_back({
.location = 1,
.binding = 0,
.format = VK_FORMAT_R32G32B32_SFLOAT,
.offset = offsetof(T, color),
});
}
else if constexpr (std::is_same_v<T, Vertex_P_N_C>)
{
desc.push_back({
.location = 0,
.binding = 0,
.format = VK_FORMAT_R32G32B32_SFLOAT,
.offset = offsetof(T, position),
});
desc.push_back({
.location = 1,
.binding = 0,
.format = VK_FORMAT_R32G32B32_SFLOAT,
.offset = offsetof(T, normal),
});
desc.push_back({
.location = 2,
.binding = 0,
.format = VK_FORMAT_R32G32B32_SFLOAT,
.offset = offsetof(T, color),
});
}
else if constexpr (std::is_same_v<T, VertexEmpty>)
{
// nada
}
else
{
static_assert(false, "Unhandled type for attribute descriptions");
}
return desc;
}
} // namespace legs

View file

@ -0,0 +1,227 @@
#pragma once
#include <memory>
#include "vulkan/vulkan_core.h"
#include <legs/log.hpp>
#include <legs/renderer/descriptor_set.hpp>
#include <legs/renderer/device.hpp>
namespace legs
{
template<class V>
class Pipeline
{
public:
Pipeline() = delete;
Pipeline(
const Device& device,
std::shared_ptr<DescriptorSet> descriptorSet,
std::vector<VkPipelineShaderStageCreateInfo> shaderStages,
bool enableCulling = true,
bool enableDepth = true
);
~Pipeline();
Pipeline(const Pipeline&) = delete;
Pipeline(Pipeline&&) = default;
Pipeline& operator=(const Pipeline&) = delete;
Pipeline& operator=(Pipeline&&) = delete;
void Bind(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint, uint32_t frameIndex);
VkPipeline GetVkPipeline() const
{
return m_vkPipeline;
}
private:
const Device& m_device;
std::shared_ptr<DescriptorSet> m_descriptorSet;
VkPipelineLayout m_vkPipelineLayout;
VkPipeline m_vkPipeline;
};
template<typename V>
Pipeline<V>::Pipeline(
const Device& device,
std::shared_ptr<DescriptorSet> descriptorSet,
std::vector<VkPipelineShaderStageCreateInfo> shaderStages,
bool enableCulling,
bool enableDepth
) :
m_device(device),
m_descriptorSet(descriptorSet)
{
LOG_DEBUG("Creating Pipeline");
const VkExtent2D swapchainExtent = m_device.GetSwapchainExtent();
const std::vector dynamicStates = {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR,
};
VkPipelineDynamicStateCreateInfo dynamicState {};
dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicState.dynamicStateCount = static_cast<uint32_t>(dynamicStates.size());
dynamicState.pDynamicStates = dynamicStates.data();
const auto vertexBindingDescription = GetBindingDescription<V>();
const auto vertexAttributeDescriptions = GetAttributeDescriptions<V>();
VkPipelineVertexInputStateCreateInfo vertexInputState {};
vertexInputState.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputState.vertexBindingDescriptionCount = 1;
vertexInputState.vertexAttributeDescriptionCount =
static_cast<uint32_t>(vertexAttributeDescriptions.size());
vertexInputState.pVertexBindingDescriptions = &vertexBindingDescription;
vertexInputState.pVertexAttributeDescriptions = vertexAttributeDescriptions.data();
VkPipelineInputAssemblyStateCreateInfo inputAssemblyState {};
inputAssemblyState.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssemblyState.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
inputAssemblyState.primitiveRestartEnable = VK_FALSE;
VkViewport viewport {};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = static_cast<float>(swapchainExtent.width);
viewport.height = static_cast<float>(swapchainExtent.height);
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
VkRect2D scissor {};
scissor.extent = swapchainExtent;
scissor.offset = {0, 0};
VkPipelineViewportStateCreateInfo viewportState {};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports = &viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &scissor;
VkPipelineRasterizationStateCreateInfo rasterizationState {};
rasterizationState.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizationState.depthClampEnable = VK_FALSE;
rasterizationState.rasterizerDiscardEnable = VK_FALSE;
rasterizationState.polygonMode = VK_POLYGON_MODE_FILL;
rasterizationState.lineWidth = 1.0f;
rasterizationState.cullMode = enableCulling ? VK_CULL_MODE_BACK_BIT : VK_CULL_MODE_NONE;
rasterizationState.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
rasterizationState.depthBiasEnable = VK_FALSE;
VkPipelineMultisampleStateCreateInfo multisampleState {};
multisampleState.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampleState.sampleShadingEnable = VK_FALSE;
multisampleState.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
VkPipelineColorBlendAttachmentState colorBlendAttachment {};
colorBlendAttachment.blendEnable = VK_TRUE;
colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT
| VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD;
colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD;
VkPipelineColorBlendStateCreateInfo colorBlendState {};
colorBlendState.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlendState.logicOpEnable = VK_FALSE;
colorBlendState.attachmentCount = 1;
colorBlendState.pAttachments = &colorBlendAttachment;
auto descriptorSetLayouts = descriptorSet->GetLayouts();
VkPipelineLayoutCreateInfo pipelineLayoutInfo {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = static_cast<uint32_t>(descriptorSetLayouts.size());
pipelineLayoutInfo.pSetLayouts = descriptorSetLayouts.data();
VK_CHECK(
vkCreatePipelineLayout(
m_device.GetVkDevice(),
&pipelineLayoutInfo,
nullptr,
&m_vkPipelineLayout
),
"Failed to create pipeline layout"
);
VkPipelineDepthStencilStateCreateInfo depthStencilState {};
depthStencilState.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
depthStencilState.depthTestEnable = enableDepth ? VK_TRUE : VK_FALSE;
depthStencilState.depthWriteEnable = enableDepth ? VK_TRUE : VK_FALSE;
depthStencilState.depthCompareOp = VK_COMPARE_OP_LESS;
depthStencilState.depthBoundsTestEnable = VK_FALSE;
depthStencilState.minDepthBounds = 0.0f;
depthStencilState.maxDepthBounds = 1.0f;
depthStencilState.stencilTestEnable = VK_FALSE;
depthStencilState.front = {};
depthStencilState.back = {};
VkFormat colorFormat = m_device.GetSwapchainImageFormat();
VkFormat depthFormat = m_device.GetDepthFormat();
VkPipelineRenderingCreateInfoKHR pipelineCreateInfo {};
pipelineCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR;
pipelineCreateInfo.pNext = VK_NULL_HANDLE;
pipelineCreateInfo.colorAttachmentCount = 1;
pipelineCreateInfo.pColorAttachmentFormats = &colorFormat;
pipelineCreateInfo.depthAttachmentFormat = depthFormat;
pipelineCreateInfo.stencilAttachmentFormat = VK_FORMAT_UNDEFINED;
VkGraphicsPipelineCreateInfo graphicsCreateInfo {};
graphicsCreateInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
graphicsCreateInfo.pNext = &pipelineCreateInfo;
graphicsCreateInfo.renderPass = VK_NULL_HANDLE;
graphicsCreateInfo.pVertexInputState = &vertexInputState;
graphicsCreateInfo.pInputAssemblyState = &inputAssemblyState;
graphicsCreateInfo.pViewportState = &viewportState;
graphicsCreateInfo.pRasterizationState = &rasterizationState;
graphicsCreateInfo.pMultisampleState = &multisampleState;
graphicsCreateInfo.pDepthStencilState = &depthStencilState;
graphicsCreateInfo.pColorBlendState = &colorBlendState;
graphicsCreateInfo.pDynamicState = &dynamicState;
graphicsCreateInfo.stageCount = static_cast<uint32_t>(shaderStages.size());
graphicsCreateInfo.pStages = shaderStages.data();
graphicsCreateInfo.layout = m_vkPipelineLayout;
VK_CHECK(
vkCreateGraphicsPipelines(
m_device.GetVkDevice(),
VK_NULL_HANDLE,
1,
&graphicsCreateInfo,
nullptr,
&m_vkPipeline
),
"Failed to create graphics pipeline"
);
}
template<typename V>
Pipeline<V>::~Pipeline()
{
LOG_DEBUG("Destroying Pipeline");
vkDestroyPipeline(m_device.GetVkDevice(), m_vkPipeline, nullptr);
vkDestroyPipelineLayout(m_device.GetVkDevice(), m_vkPipelineLayout, nullptr);
}
template<typename V>
void Pipeline<V>::Bind(
VkCommandBuffer commandBuffer,
VkPipelineBindPoint bindPoint,
uint32_t frameIndex
)
{
vkCmdBindPipeline(commandBuffer, bindPoint, m_vkPipeline);
m_descriptorSet->Bind(commandBuffer, bindPoint, m_vkPipelineLayout, frameIndex);
}
} // namespace legs

View file

@ -0,0 +1,183 @@
#pragma once
#include <memory>
#include <stdexcept>
#include <imgui_impl_vulkan.h>
#include <legs/entity/camera.hpp>
#include <legs/renderer/buffer.hpp>
#include <legs/renderer/common.hpp>
#include <legs/renderer/descriptor_set.hpp>
#include <legs/renderer/device.hpp>
#include <legs/renderer/instance.hpp>
#include <legs/renderer/pipeline.hpp>
#include <legs/renderer/ubo.hpp>
namespace legs
{
enum RenderPipeline
{
INVALID,
GEO_P_C,
GEO_P_N_C,
FULLSCREEN,
SKY,
};
class Renderer
{
public:
Renderer(std::shared_ptr<Window> window);
~Renderer();
Renderer(const Renderer&) = delete;
Renderer(Renderer&&) = delete;
Renderer& operator=(const Renderer&) = delete;
Renderer& operator=(Renderer&&) = delete;
void SetWindow(std::shared_ptr<Window> window);
void ResetViewport()
{
m_device.ResetViewport();
}
void SetViewport(SRect rect)
{
m_device.SetViewport(rect);
}
void ClearViewport();
void Resize();
float GetAspect();
void Begin();
void Submit();
void Present();
void UpdateUBO();
void WaitForIdle();
std::shared_ptr<UniformBufferObject> GetUBO()
{
return m_ubo;
}
void* GetCommandBuffer()
{
return GetVkCommandBuffer();
};
void GetImGuiInfo(ImGuiCreationInfo& info);
VkCommandBuffer GetVkCommandBuffer() const
{
return m_device.GetCommandBuffer();
}
VkCommandBuffer GetTemporaryCommandBuffer()
{
return m_device.GetTemporaryCommandBuffer();
}
void SubmitTemporaryCommandBuffer(VkCommandBuffer buffer)
{
m_device.SubmitTemporaryCommandBuffer(buffer);
}
void CreateBuffer(
std::shared_ptr<Buffer>& buffer,
BufferType bufferType,
void* data,
uint32_t elementSize,
uint32_t elementCount
)
{
auto hostBuffer =
std::make_shared<Buffer>(bufferType, HostBuffer, elementSize, elementCount);
auto deviceBuffer =
std::make_shared<Buffer>(bufferType, DeviceBuffer, elementSize, elementCount);
hostBuffer->Write(data, elementSize * elementCount);
auto tempBuffer = GetTemporaryCommandBuffer();
hostBuffer->CopyToDevice(tempBuffer, static_pointer_cast<Buffer>(deviceBuffer));
SubmitTemporaryCommandBuffer(tempBuffer);
buffer = static_pointer_cast<Buffer>(deviceBuffer);
}
void DrawWithBuffers(std::shared_ptr<Buffer> vertexBuffer, std::shared_ptr<Buffer> indexBuffer)
{
auto commandBuffer = GetCommandBuffer();
if (commandBuffer != nullptr)
{
vertexBuffer->Bind(commandBuffer);
indexBuffer->Bind(commandBuffer);
indexBuffer->Draw(commandBuffer);
m_frameBuffers.push_back(vertexBuffer);
m_frameBuffers.push_back(indexBuffer);
}
}
void BindPipeline(RenderPipeline pipe)
{
auto commandBuffer = m_device.GetCommandBuffer();
auto currentFrame = m_device.GetCurrentFrame();
switch (pipe)
{
case GEO_P_C:
{
m_testPipeline->Bind(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, currentFrame);
break;
}
case GEO_P_N_C:
{
m_geoPNCPipeline
->Bind(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, currentFrame);
break;
}
case FULLSCREEN:
{
m_fullscreenPipeline
->Bind(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, currentFrame);
break;
}
case SKY:
{
m_skyPipeline->Bind(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, currentFrame);
break;
}
default:
{
throw std::runtime_error("unknown pipeline");
}
}
}
private:
VkShaderModule& CreateShaderModule(VkShaderModuleCreateInfo createInfo);
constexpr VkPipelineShaderStageCreateInfo FillShaderStageCreateInfo(
VkShaderModule& module,
VkShaderStageFlagBits stage
);
Instance m_instance;
Device m_device;
std::shared_ptr<DescriptorSet> m_descriptorSet;
std::vector<VkShaderModule> m_vkShaderModules;
std::shared_ptr<Pipeline<Vertex_P_C>> m_testPipeline;
std::shared_ptr<Pipeline<Vertex_P_N_C>> m_geoPNCPipeline;
std::shared_ptr<Pipeline<VertexEmpty>> m_fullscreenPipeline;
std::shared_ptr<Pipeline<Vertex_P>> m_skyPipeline;
std::shared_ptr<UniformBufferObject> m_ubo;
// Hold so we don't call Buffer destructor
// while still in use by command buffer.
std::vector<std::shared_ptr<Buffer>> m_frameBuffers;
};
} // namespace legs

View file

@ -0,0 +1,41 @@
#pragma once
#include <vulkan/vulkan_core.h>
// Generated files by glslang
#include <fullscreen_frag.h>
#include <fullscreen_vert.h>
#include <lit_pnc_vert.h>
#include <lit_pnc_frag.h>
#include <unlit_pc_frag.h>
#include <unlit_pc_vert.h>
#include <sky_frag.h>
#include <sky_vert.h>
namespace legs
{
class Shader
{
public:
Shader() = delete;
~Shader() = delete;
Shader(const Shader&) = delete;
Shader(Shader&&) = delete;
Shader& operator=(const Shader&) = delete;
Shader& operator=(Shader&&) = delete;
constexpr static VkShaderModuleCreateInfo GetCreateInfo(const uint32_t spv[], const size_t size)
{
VkShaderModuleCreateInfo createInfo {};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = size;
createInfo.pCode = static_cast<const uint32_t*>(spv);
return createInfo;
}
};
// Usage:
// auto shader = LOAD_VULKAN_SPV(simple_frag);
// To load Shaders/simple_frag.frag (simple_frag.h)
#define LOAD_VULKAN_SPV(NAME) legs::Shader::GetCreateInfo(NAME, sizeof(NAME))
} // namespace legs

View file

@ -0,0 +1,50 @@
#pragma once
#include <glm/ext/matrix_clip_space.hpp>
#include <glm/ext/matrix_transform.hpp>
#include <glm/ext/quaternion_common.hpp>
#include <glm/ext/quaternion_float.hpp>
#include <glm/ext/quaternion_transform.hpp>
#include <glm/gtc/constants.hpp>
#include <glm/mat4x4.hpp>
#include <glm/matrix.hpp>
#include <glm/vec3.hpp>
#include <legs/entity/camera.hpp>
namespace legs
{
struct UniformBufferObject
{
alignas(16) glm::mat4 model;
alignas(16) glm::mat4 view;
alignas(16) glm::mat4 proj;
alignas(16) glm::mat4 mvp;
alignas(16) glm::mat4 invModel;
alignas(16) glm::mat4 invView;
alignas(16) glm::mat4 invProj;
alignas(16) glm::mat4 clipToWorld;
alignas(16) glm::vec3 eye;
alignas(16) glm::vec4 viewport;
alignas(16) glm::vec3 sunDir;
alignas(16) glm::vec3 sunColor;
void SetCamera(const std::shared_ptr<Camera> cam)
{
model = glm::identity<glm::mat4>();
view = cam->view;
proj = cam->proj;
mvp = proj * view * model;
invModel = glm::inverse(model);
invView = glm::inverse(view);
invProj = glm::inverse(proj);
clipToWorld = glm::inverse(proj * view);
eye = cam->GetPosition();
viewport = cam->viewport;
}
};
}; // namespace legs

View file

@ -0,0 +1,14 @@
#pragma once
#include <vk_mem_alloc.h>
namespace legs
{
extern void CreateAllocator(VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice device);
extern void DestroyAllocator();
extern VmaTotalStatistics GetAllocatorTotalStatistics();
extern VmaAllocator g_vma;
} // namespace legs

View file

@ -0,0 +1,12 @@
#version 450
#extension GL_GOOGLE_include_directive : enable
layout(location = 0) in vec2 outUV;
layout(location = 0) out vec4 outColor;
void main()
{
outColor = vec4(0.0, 0.0, 0.0, 1.0);
}

View file

@ -0,0 +1,12 @@
// https://www.saschawillems.de/blog/2016/08/13/vulkan-tutorial-on-rendering-a-fullscreen-quad-without-buffers/
#version 450
#extension GL_GOOGLE_include_directive : enable
layout(location = 0) out vec2 outUV;
void main()
{
outUV = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
gl_Position = vec4(outUV * 2.0f + -1.0f, 0.0f, 1.0f);
}

View file

@ -0,0 +1,21 @@
vec3 BlinnPhong(vec3 pos, vec3 normal, vec3 eyePos, float specularity)
{
vec3 viewDir = normalize(eyePos - pos);
vec3 halfDir = normalize(ubo.sunDir + viewDir);
float sunMult = smoothstep(-0.6, 0.1, ubo.sunDir.z);
vec3 sunColor = ubo.sunColor * sunMult;
float diff = max(dot(normal, ubo.sunDir), 0.0);
vec3 diffuse = diff * sunColor;
float spec = pow(max(dot(normal, halfDir), 0.0), 16.0);
vec3 specular = specularity * spec * sunColor;
vec3 ambient = vec3(0.01);
float horizonScatter = 1.0 - abs(ubo.sunDir.z);
ambient += 0.08 * horizonScatter;
ambient.r *= (1.0 + horizonScatter);
return ambient + diffuse + specular;
}

View file

@ -0,0 +1,17 @@
layout(binding = 0) uniform UniformBufferObject
{
mat4 model;
mat4 view;
mat4 proj;
mat4 mvp;
mat4 invModel;
mat4 invView;
mat4 invProj;
mat4 clipToWorld;
vec3 eye;
vec4 viewport;
vec3 sunDir;
vec3 sunColor;
} ubo;

View file

@ -0,0 +1,14 @@
vec4 FragClipPos()
{
vec4 ndc;
ndc.xy = (2.0 * gl_FragCoord.xy / ubo.viewport.xy) - 1.0;
ndc.z = (2.0 * gl_FragCoord.z) - 1.0;
ndc.w = 1.0;
vec4 clip = ndc / gl_FragCoord.w;
return clip;
}
vec3 FragWorldPos()
{
return (ubo.clipToWorld * FragClipPos()).xyz;
}

View file

@ -0,0 +1 @@
layout(location = 0) in vec3 inPosition;

View file

@ -0,0 +1,2 @@
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;

View file

@ -0,0 +1,3 @@
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inNormal;
layout(location = 2) in vec3 inColor;

View file

@ -0,0 +1,15 @@
#version 450
#extension GL_GOOGLE_include_directive : enable
#include "include/ubo.glsl"
#include "include/lighting.glsl"
layout(location = 0) in vec3 fragColor;
layout(location = 0) out vec4 outColor;
void main()
{
outColor = vec4(fragColor, 1.0);
}

View file

@ -0,0 +1,19 @@
#version 450
#extension GL_GOOGLE_include_directive : enable
#include "include/vertex_pnc.glsl"
#include "include/ubo.glsl"
#include "include/lighting.glsl"
layout(location = 0) out vec3 fragColor;
void main()
{
gl_Position = ubo.mvp * vec4(inPosition, 1.0);
vec3 position = (ubo.model * vec4(inPosition, 1.0)).xyz;
vec3 normal = (ubo.model * vec4(inNormal, 1.0)).xyz;
vec3 light = BlinnPhong(gl_Position.xyz, normal, ubo.eye, 1.0);
fragColor = inColor * light;
}

View file

@ -0,0 +1,40 @@
#version 450
#extension GL_GOOGLE_include_directive : enable
#include "include/ubo.glsl"
#include "include/util.glsl"
layout(location = 0) in vec3 fragDir;
layout(location = 0) out vec4 outColor;
void main()
{
float sunDot = dot(fragDir, ubo.sunDir);
vec3 darkColor = vec3(0.05, 0.05, 0.05);
vec3 baseColor = vec3(0.1, 0.2, 0.7);
vec3 brightColor = vec3(1.0, 1.0, 1.0);
float dayAmount = 0.01 + smoothstep(-1.0, 1.0, ubo.sunDir.z);
darkColor.rgb *= dayAmount;
baseColor.rgb *= dayAmount;
brightColor.rgb *= dayAmount;
float horizonAmount = 3.0 * smoothstep(0.9, -0.4, ubo.sunDir.z);
vec3 horizonColor = 1.0 + vec3(horizonAmount, 0.0, -0.4 * horizonAmount);
darkColor.rgb *= horizonColor;
baseColor.rgb *= horizonColor;
brightColor.rgb *= horizonColor;
float darkMix = smoothstep(0.0, -1.0, sunDot);
float brightMix = sign(sunDot) * pow(sunDot, 4);
brightMix = smoothstep(0.0, 1.0, brightMix);
vec3 gradient = mix(baseColor, darkColor, darkMix)
+ mix(baseColor, brightColor, brightMix);
outColor = vec4(gradient, 1.0);
}

View file

@ -0,0 +1,14 @@
#version 450
#extension GL_GOOGLE_include_directive : enable
#include "include/vertex_p.glsl"
#include "include/ubo.glsl"
layout(location = 0) out vec3 fragDir;
void main()
{
fragDir = normalize((transpose(ubo.view) * vec4(inPosition, 1.0)).xyz);
gl_Position = ubo.proj * vec4(inPosition, 1.0);
}

View file

@ -0,0 +1,12 @@
#version 450
#extension GL_GOOGLE_include_directive : enable
layout(location = 0) in vec3 fragColor;
layout(location = 0) out vec4 outColor;
void main()
{
outColor = vec4(fragColor, 1.0);
}

View file

@ -0,0 +1,14 @@
#version 450
#extension GL_GOOGLE_include_directive : enable
#include "include/vertex_pc.glsl"
#include "include/ubo.glsl"
layout(location = 0) out vec3 fragColor;
void main()
{
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0);
fragColor = inColor;
}

108
src/public/legs/time.hpp Normal file
View file

@ -0,0 +1,108 @@
#pragma once
#include <chrono>
namespace legs
{
using clock_duration = std::chrono::duration<
double,
std::ratio<
std::chrono::high_resolution_clock::period::num,
std::chrono::high_resolution_clock::period::den>>;
class Time
{
public:
static double Now()
{
const auto now = std::chrono::high_resolution_clock::now();
return static_cast<double>(now.time_since_epoch().count())
* std::chrono::high_resolution_clock::period::num
/ std::chrono::high_resolution_clock::period::den;
}
static double Uptime()
{
return Now() - startTime;
}
constexpr static clock_duration Duration(const double seconds)
{
return clock_duration {
seconds * std::chrono::high_resolution_clock::period::den
/ std::chrono::high_resolution_clock::period::num
};
}
static void UpdateFrameDelta()
{
const auto now = Now();
DeltaFrame = now - prevFrame;
prevFrame = now;
}
static void UpdateTickDelta()
{
const auto now = Now();
DeltaTick = now - prevTick;
prevTick = now;
}
static double TimeSinceEngineFrame()
{
return Now() - prevFrame;
}
static double TimeSinceEngineTick()
{
return Now() - prevTick;
}
static double TimeToEngineFrame()
{
return FrameInterval - TimeSinceEngineFrame();
}
static double TimeToEngineTick()
{
return TickInterval - TimeSinceEngineTick();
}
static void SetStart()
{
startTime = Now();
}
static void SetFrameRate(unsigned int fps)
{
FrameRate = fps;
FrameInterval = 1.0 / fps;
}
static void StartRender()
{
renderStart = Now();
}
static void StopRender()
{
DeltaRender = Now() - renderStart;
}
static inline double DeltaFrame;
static inline double DeltaTick;
static inline double DeltaRender;
static inline unsigned int FrameRate = 60;
static inline double FrameInterval = 1.0 / FrameRate;
static constexpr const unsigned int TickRate = 60;
static constexpr const double TickInterval = 1.0 / TickRate;
private:
static inline double prevFrame;
static inline double prevTick;
static inline double renderStart;
static inline double startTime;
};
} // namespace legs

54
src/public/legs/ui/ui.hpp Normal file
View file

@ -0,0 +1,54 @@
#pragma once
#include <memory>
#include <glm/vec2.hpp>
#include <legs/renderer/renderer.hpp>
#include <legs/window/window.hpp>
namespace legs
{
enum class UIWindow
{
DEBUG,
DEMO,
MAX
};
struct UIState
{
bool showWindow[static_cast<unsigned int>(UIWindow::MAX)];
};
class UI
{
public:
UI() = delete;
UI(const UI&) = delete;
UI(UI&&) = delete;
UI& operator=(const UI&) = delete;
UI& operator=(UI&&) = delete;
UI(std::shared_ptr<Window> window, std::shared_ptr<Renderer> renderer);
~UI();
void ToggleWindow(UIWindow window)
{
auto index = static_cast<unsigned int>(window);
m_state.showWindow[index] = !m_state.showWindow[index];
}
void Render();
private:
void DebugWindow();
void DemoWindow();
std::shared_ptr<Window> m_window;
std::shared_ptr<Renderer> m_renderer;
ImGuiCreationInfo m_info;
UIState m_state;
};
}; // namespace legs

View file

@ -0,0 +1,130 @@
#pragma once
#include <glm/vec2.hpp>
#include <SDL_scancode.h>
namespace legs
{
enum Key : unsigned int
{
KEY_NONE,
KEY_MOVE_FORWARD,
KEY_MOVE_BACK,
KEY_MOVE_RIGHT,
KEY_MOVE_LEFT,
KEY_MOVE_UP,
KEY_MOVE_DOWN,
KEY_MOUSE_GRAB,
KEY_WINDOW_DEBUG,
KEY_WINDOW_DEMO,
KEY_MAX,
};
static constexpr unsigned long long KeyToFlag(Key k)
{
return 1 << static_cast<unsigned int>(k);
}
class InputSettings
{
public:
InputSettings()
{
for (unsigned int i = 0; i < SDL_NUM_SCANCODES; i++)
{
m_sdlKeyMap[i] = Key::KEY_NONE;
}
ApplyDefaults();
}
void ApplyDefaults()
{
m_sdlKeyMap[static_cast<unsigned int>(SDL_SCANCODE_W)] = Key::KEY_MOVE_FORWARD;
m_sdlKeyMap[static_cast<unsigned int>(SDL_SCANCODE_A)] = Key::KEY_MOVE_LEFT;
m_sdlKeyMap[static_cast<unsigned int>(SDL_SCANCODE_S)] = Key::KEY_MOVE_BACK;
m_sdlKeyMap[static_cast<unsigned int>(SDL_SCANCODE_D)] = Key::KEY_MOVE_RIGHT;
m_sdlKeyMap[static_cast<unsigned int>(SDL_SCANCODE_SPACE)] = Key::KEY_MOVE_UP;
m_sdlKeyMap[static_cast<unsigned int>(SDL_SCANCODE_LCTRL)] = Key::KEY_MOVE_DOWN;
m_sdlKeyMap[static_cast<unsigned int>(SDL_SCANCODE_F1)] = Key::KEY_MOUSE_GRAB;
m_sdlKeyMap[static_cast<unsigned int>(SDL_SCANCODE_F2)] = Key::KEY_WINDOW_DEBUG;
m_sdlKeyMap[static_cast<unsigned int>(SDL_SCANCODE_F3)] = Key::KEY_WINDOW_DEMO;
}
Key GetKeyFromSDL(unsigned int scan)
{
return m_sdlKeyMap[scan];
}
private:
Key m_sdlKeyMap[SDL_NUM_SCANCODES];
};
struct WindowInput
{
bool wantsQuit;
bool wantsResize;
unsigned long long keyFlags;
glm::ivec2 mouse;
glm::ivec2 scroll;
WindowInput()
{
Clear(true);
}
void Clear(bool clearKeys = false)
{
wantsQuit = false;
wantsResize = false;
// Generally don't want to do this,
// since window only tracks key up / down events.
if (clearKeys)
{
keyFlags = 0;
}
mouse.x = 0;
mouse.y = 0;
scroll.x = 0;
scroll.y = 0;
}
void Aggregate(const WindowInput& other)
{
wantsQuit |= other.wantsQuit;
wantsResize |= other.wantsResize;
keyFlags |= other.keyFlags;
mouse += other.mouse;
scroll += other.scroll;
}
bool HasKey(Key key)
{
return (keyFlags & KeyToFlag(key)) == KeyToFlag(key);
}
void KeyDown(Key key)
{
keyFlags |= KeyToFlag(key);
}
void KeyUp(Key key)
{
keyFlags &= ~KeyToFlag(key);
}
};
}; // namespace legs

View file

@ -0,0 +1,43 @@
#pragma once
#include <memory>
#include <SDL_syswm.h>
#include <vulkan/vulkan_core.h>
#include <legs/window/input.hpp>
namespace legs
{
class Window
{
public:
Window(std::shared_ptr<InputSettings> inputSettings);
~Window();
void SetTitle(const char* title);
void SetMouseGrab(bool grab);
bool IsMouseGrabbed();
void AggregateInput(WindowInput& input);
void GetFramebufferSize(int* width, int* height);
bool CreateSurface(VkInstance instance, VkSurfaceKHR* surface);
bool GetExtensions(unsigned int* pCount, const char** pNames);
bool IsMinimized();
unsigned int GetRefreshRate();
SDL_Window* GetSDLWindow() const
{
return m_window;
}
private:
SDL_Window* m_window;
std::shared_ptr<InputSettings> m_inputSettings;
};
} // namespace legs

View file

@ -0,0 +1,58 @@
#pragma once
#include <memory>
#include <mutex>
#include <legs/iphysics.hpp>
#include <legs/entity/mesh_entity.hpp>
#include <legs/entity/sky.hpp>
namespace legs
{
class World
{
public:
World() = delete;
World(std::shared_ptr<Renderer> renderer);
~World();
World(const World&) = delete;
World(World&&) = delete;
World& operator=(const World&) = delete;
World& operator=(World&&) = delete;
void Frame();
void Tick();
void Render();
void AddEntity(std::shared_ptr<Entity> entity);
void RemoveEntity(std::shared_ptr<Entity> entity);
void SetSky(std::shared_ptr<Sky> sky)
{
m_sky = sky;
}
std::shared_ptr<Sky> GetSky()
{
return m_sky;
}
std::shared_ptr<IPhysics> GetPhysics()
{
return m_physics;
}
private:
std::mutex m_worldMutex;
std::shared_ptr<Renderer> m_renderer;
std::vector<std::shared_ptr<Entity>> m_entities;
std::shared_ptr<Sky> m_sky;
std::shared_ptr<IPhysics> m_physics;
};
} // namespace legs

253
src/renderer/buffer.cpp Normal file
View file

@ -0,0 +1,253 @@
#include <legs/renderer/buffer.hpp>
namespace legs
{
Buffer::Buffer(
BufferType bufferType,
BufferLocation bufferLocation,
uint32_t elementSize,
uint32_t elementCount
) :
m_bufferType(bufferType),
m_bufferLocation(bufferLocation),
m_elementSize(elementSize),
m_elementCount(elementCount),
m_size(m_elementSize * m_elementCount)
{
VkBufferCreateInfo bufferInfo {};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = m_size;
bufferInfo.usage = 0;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VmaAllocationCreateInfo allocInfo {};
switch (m_bufferType)
{
case VertexBuffer:
{
bufferInfo.usage |= VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;
break;
}
case IndexBuffer:
{
bufferInfo.usage |= VK_BUFFER_USAGE_INDEX_BUFFER_BIT;
break;
}
case UniformBuffer:
{
bufferInfo.usage |= VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
allocInfo.requiredFlags |= VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
break;
}
default:
{
std::runtime_error("Unhandled buffer type");
}
}
switch (m_bufferLocation)
{
case HostBuffer:
{
bufferInfo.usage |= VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
allocInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST;
allocInfo.flags |= VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
allocInfo.requiredFlags |= VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT;
allocInfo.preferredFlags |= VK_MEMORY_PROPERTY_HOST_CACHED_BIT;
break;
}
case DeviceBuffer:
{
bufferInfo.usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT;
allocInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE;
allocInfo.preferredFlags |= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
break;
}
default:
{
throw std::runtime_error("Unhandled buffer destination");
}
}
vmaCreateBuffer(g_vma, &bufferInfo, &allocInfo, &m_vkBuffer, &m_vmaAllocation, nullptr);
}
Buffer::~Buffer()
{
if (m_isMapped)
{
Unmap();
}
vmaDestroyBuffer(g_vma, m_vkBuffer, m_vmaAllocation);
}
void Buffer::Write(void* data, size_t size)
{
vmaCopyMemoryToAllocation(g_vma, data, m_vmaAllocation, 0, size);
}
void Buffer::CopyToDevice(void* commandBuffer, std::shared_ptr<Buffer> deviceBuffer)
{
if (m_bufferLocation != HostBuffer)
{
throw std::runtime_error("Tried copying from a non-host buffer");
}
if (deviceBuffer->GetLocation() != DeviceBuffer)
{
throw std::runtime_error("Tried copying to a non-device buffer");
}
VkDeviceSize requiredSize = GetElementCount() * GetElementSize();
if (deviceBuffer->GetSize() < requiredSize)
{
throw std::runtime_error("Tried copying to a buffer that is too small");
}
auto vkCommandBuffer = static_cast<VkCommandBuffer>(commandBuffer);
VkBufferCopy copyRegion {};
copyRegion.size = requiredSize;
copyRegion.srcOffset = 0;
copyRegion.dstOffset = 0;
auto vkDeviceBuffer = static_pointer_cast<Buffer>(deviceBuffer);
vkCmdCopyBuffer(vkCommandBuffer, m_vkBuffer, vkDeviceBuffer->GetVkBuffer(), 1, &copyRegion);
deviceBuffer->SetElementCount(m_elementCount);
}
void Buffer::Clear()
{
if (m_bufferLocation != HostBuffer)
{
throw std::runtime_error("Tried mapping a non-host buffer");
}
VmaAllocationInfo allocInfo {};
vmaGetAllocationInfo(g_vma, m_vmaAllocation, &allocInfo);
void* mappedData;
vmaMapMemory(g_vma, m_vmaAllocation, &mappedData);
std::memset(mappedData, 0, allocInfo.size);
vmaUnmapMemory(g_vma, m_vmaAllocation);
if (!(allocInfo.memoryType & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT))
{
vmaFlushAllocation(g_vma, m_vmaAllocation, 0, allocInfo.size);
}
}
void Buffer::Bind(void* commandBuffer)
{
if (m_bufferLocation != DeviceBuffer)
{
throw std::runtime_error("Tried to bind a non-device buffer");
}
auto vkCommandBuffer = static_cast<VkCommandBuffer>(commandBuffer);
switch (m_bufferType)
{
case VertexBuffer:
{
VkBuffer buffers[] = {m_vkBuffer};
VkDeviceSize offsets[] = {0};
vkCmdBindVertexBuffers(vkCommandBuffer, 0, 1, buffers, offsets);
break;
}
case IndexBuffer:
{
vkCmdBindIndexBuffer(vkCommandBuffer, m_vkBuffer, 0, VK_INDEX_TYPE_UINT32);
break;
}
default:
{
throw std::runtime_error("Unhandled buffer type");
}
}
}
void Buffer::Draw(void* commandBuffer)
{
auto vkCommandBuffer = static_cast<VkCommandBuffer>(commandBuffer);
switch (m_bufferType)
{
case VertexBuffer:
{
vkCmdDraw(vkCommandBuffer, m_elementCount, 1, 0, 0);
break;
}
case IndexBuffer:
{
vkCmdDrawIndexed(vkCommandBuffer, m_elementCount, 1, 0, 0, 0);
break;
}
default:
{
throw std::runtime_error("Unhandled buffer type");
}
}
}
void Buffer::Draw(
void* commandBuffer,
uint32_t indexOffset,
uint32_t indexCount,
int32_t vertexOffset
)
{
auto vkCommandBuffer = static_cast<VkCommandBuffer>(commandBuffer);
switch (m_bufferType)
{
case IndexBuffer:
{
vkCmdDrawIndexed(vkCommandBuffer, indexCount, 1, indexOffset, vertexOffset, 0);
break;
}
default:
{
throw std::runtime_error("Unhandled buffer type");
}
}
}
void Buffer::Map(void** data)
{
if (m_isMapped)
{
throw std::runtime_error("Tried to map a buffer that is already mapped");
}
vmaMapMemory(g_vma, m_vmaAllocation, data);
m_isMapped = true;
}
void Buffer::Unmap()
{
if (!m_isMapped)
{
throw std::runtime_error("Tried to unmap buffer that isn't mapped");
}
vmaUnmapMemory(g_vma, m_vmaAllocation);
m_isMapped = false;
}
}; // namespace legs

View file

@ -0,0 +1,116 @@
#include <cstdint>
#include <cstring>
#include <legs/renderer/common.hpp>
#include <legs/renderer/descriptor_set.hpp>
#include <legs/renderer/mesh_data.hpp>
#include <legs/renderer/ubo.hpp>
namespace legs
{
DescriptorSet::DescriptorSet(
const Device& device,
std::vector<std::shared_ptr<Buffer>> uboBuffers
) :
m_device(device),
m_uniformBuffers(uboBuffers)
{
VkDescriptorSetLayoutBinding uboBinding {};
uboBinding.binding = 0;
uboBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
uboBinding.descriptorCount = 1;
uboBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
uboBinding.pImmutableSamplers = nullptr;
VkDescriptorSetLayoutCreateInfo layoutInfo {};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = 1;
layoutInfo.pBindings = &uboBinding;
const auto maxFrames = static_cast<uint32_t>(m_uniformBuffers.size());
m_vkLayouts.resize(maxFrames);
for (uint32_t i = 0; i < maxFrames; i++)
{
VK_CHECK(
vkCreateDescriptorSetLayout(
device.GetVkDevice(),
&layoutInfo,
nullptr,
&m_vkLayouts[i]
),
"Failed to create descriptor set layout"
);
}
m_vkSets.resize(maxFrames);
VkDescriptorSetAllocateInfo allocInfo {};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = device.GetUboDescriptorPool();
allocInfo.descriptorSetCount = maxFrames;
allocInfo.pSetLayouts = m_vkLayouts.data();
VK_CHECK(
vkAllocateDescriptorSets(device.GetVkDevice(), &allocInfo, m_vkSets.data()),
"Failed to allocate descriptor sets"
);
m_ubosMappedMemory.resize(maxFrames);
for (uint32_t i = 0; i < maxFrames; i++)
{
auto uniformBuffer = uboBuffers[i];
uniformBuffer->Map(&m_ubosMappedMemory[i]);
VkDescriptorBufferInfo bufferInfo {};
bufferInfo.buffer = uniformBuffer->GetVkBuffer();
bufferInfo.offset = 0;
bufferInfo.range = VK_WHOLE_SIZE;
VkWriteDescriptorSet descriptorWrite {};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = m_vkSets[i];
descriptorWrite.dstBinding = 0;
descriptorWrite.dstArrayElement = 0;
descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrite.descriptorCount = 1;
descriptorWrite.pBufferInfo = &bufferInfo;
vkUpdateDescriptorSets(m_device.GetVkDevice(), 1, &descriptorWrite, 0, nullptr);
}
}
DescriptorSet::~DescriptorSet()
{
m_uniformBuffers.clear();
for (auto& layout : m_vkLayouts)
{
vkDestroyDescriptorSetLayout(m_device.GetVkDevice(), layout, nullptr);
}
}
void DescriptorSet::UpdateUBO(uint32_t frameIndex, const std::shared_ptr<UniformBufferObject> ubo)
{
std::memcpy(m_ubosMappedMemory[frameIndex], ubo.get(), sizeof(UniformBufferObject));
}
void DescriptorSet::Bind(
VkCommandBuffer commandBuffer,
VkPipelineBindPoint bindPoint,
VkPipelineLayout pipelineLayout,
uint32_t frameIndex
)
{
vkCmdBindDescriptorSets(
commandBuffer,
bindPoint,
pipelineLayout,
0,
1,
&m_vkSets[frameIndex],
0,
nullptr
);
}
} // namespace legs

891
src/renderer/device.cpp Normal file
View file

@ -0,0 +1,891 @@
#include <algorithm>
#include <cstdint>
#include <limits>
#include <set>
#include <stdexcept>
#include <vector>
#include <vulkan/vulkan_core.h>
#include <legs/log.hpp>
#include <legs/renderer/common.hpp>
#include <legs/renderer/device.hpp>
namespace legs
{
constexpr static void _vkCmdBeginRenderingKHR(
VkInstance instance,
VkCommandBuffer commandBuffer,
const VkRenderingInfo* renderingInfo
)
{
auto func =
(PFN_vkCmdBeginRenderingKHR)vkGetInstanceProcAddr(instance, "vkCmdBeginRenderingKHR");
if (func != nullptr)
{
func(commandBuffer, renderingInfo);
}
else
{
throw std::runtime_error("Failed to find vkCmdBeginRenderingKHR address");
}
}
constexpr static void _vkCmdEndRenderingKHR(VkInstance instance, VkCommandBuffer commandBuffer)
{
auto func = (PFN_vkCmdEndRenderingKHR)vkGetInstanceProcAddr(instance, "vkCmdEndRenderingKHR");
if (func != nullptr)
{
func(commandBuffer);
}
else
{
throw std::runtime_error("Failed to find vkCmdEndRenderingKHR address");
}
}
Device::Device(const Instance& instance, uint32_t maxFramesInFlight) :
m_instance(instance),
m_maxFramesInFlight(maxFramesInFlight)
{
LOG_INFO("Creating Device");
PickPhysicalDevice();
CreateLogicalDevice();
CreateAllocator(m_instance.GetVkInstance(), m_vkPhysicalDevice, m_vkDevice);
CreateSwapchain();
CreateImageViews();
CreateCommandPool();
CreateCommandBuffers();
CreateSyncObjects();
CreateDescriptorPools();
}
Device::~Device()
{
LOG_INFO("Destroying Device");
vkDeviceWaitIdle(m_vkDevice);
vkDestroyDescriptorPool(m_vkDevice, m_vkUboDescriptorPool, nullptr);
vkDestroyDescriptorPool(m_vkDevice, m_vkImGuiDescriptorPool, nullptr);
for (auto& fence : m_vkInFlightFences)
{
vkDestroyFence(m_vkDevice, fence, nullptr);
}
for (auto& semaphore : m_vkImageSemaphores)
{
vkDestroySemaphore(m_vkDevice, semaphore, nullptr);
}
for (auto& semaphore : m_vkRenderSemaphores)
{
vkDestroySemaphore(m_vkDevice, semaphore, nullptr);
}
vkDestroyCommandPool(m_vkDevice, m_vkCommandPool, nullptr);
DestroySwapchain();
DestroyAllocator();
vkDestroyDevice(m_vkDevice, nullptr);
}
void Device::Begin()
{
VK_CHECK(
vkWaitForFences(m_vkDevice, 1, &m_vkInFlightFences[m_currentFrame], VK_TRUE, UINT64_MAX),
"Failed waiting for in flight fence"
);
auto imageResult = vkAcquireNextImageKHR(
m_vkDevice,
m_vkSwapchain,
UINT64_MAX,
m_vkImageSemaphores[m_currentFrame],
VK_NULL_HANDLE,
&m_currentImageIndex
);
if (imageResult == VK_ERROR_OUT_OF_DATE_KHR)
{
RecreateSwapchain();
return;
}
else if (imageResult != VK_SUCCESS && imageResult != VK_SUBOPTIMAL_KHR)
{
throw std::runtime_error("Failed to acquire next swapchain image");
}
VK_CHECK(
vkResetFences(m_vkDevice, 1, &m_vkInFlightFences[m_currentFrame]),
"Failed to reset in flight fence"
);
VK_CHECK(
vkResetCommandBuffer(m_vkCommandBuffers[m_currentFrame], 0),
"Failed to reset command buffer"
);
VkCommandBufferBeginInfo beginInfo {};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
VK_CHECK(
vkBeginCommandBuffer(m_vkCommandBuffers[m_currentFrame], &beginInfo),
"Failed to begin command buffer"
);
TransitionImageLayout(
m_vkCommandBuffers[m_currentFrame],
m_vkSwapchainImages[m_currentImageIndex],
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
);
VkClearValue clearColor {};
clearColor.color = {
{0.0f, 0.0f, 0.0f, 1.0f}
};
VkClearValue clearDepth {};
clearDepth.depthStencil = {1.0f, 0};
VkRenderingAttachmentInfoKHR colorAttachment {};
colorAttachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
colorAttachment.clearValue = clearColor;
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachment.imageView = m_vkSwapchainImageViews[m_currentImageIndex];
colorAttachment.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
colorAttachment.resolveMode = VK_RESOLVE_MODE_NONE;
VkRenderingAttachmentInfoKHR depthAttachment {};
depthAttachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
depthAttachment.clearValue = clearDepth;
depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
depthAttachment.imageView = m_vkDepthImageView;
depthAttachment.imageLayout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL;
depthAttachment.resolveMode = VK_RESOLVE_MODE_NONE;
VkRect2D renderArea {};
renderArea.extent = m_vkSwapchainExtent;
renderArea.offset = {0, 0};
VkRenderingInfo renderingInfo {};
renderingInfo.sType = VK_STRUCTURE_TYPE_RENDERING_INFO;
renderingInfo.renderArea = renderArea;
renderingInfo.viewMask = 0;
renderingInfo.layerCount = 1;
renderingInfo.colorAttachmentCount = 1;
renderingInfo.pColorAttachments = &colorAttachment;
renderingInfo.pDepthAttachment = &depthAttachment;
renderingInfo.pStencilAttachment = nullptr;
renderingInfo.flags = 0;
_vkCmdBeginRenderingKHR(
m_instance.GetVkInstance(),
m_vkCommandBuffers[m_currentFrame],
&renderingInfo
);
ResetViewport();
}
void Device::ResetViewport()
{
VkViewport viewport {};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = static_cast<float>(m_vkSwapchainExtent.width);
viewport.height = static_cast<float>(m_vkSwapchainExtent.height);
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
vkCmdSetViewport(m_vkCommandBuffers[m_currentFrame], 0, 1, &viewport);
VkRect2D scissor {};
scissor.offset = {0, 0};
scissor.extent = m_vkSwapchainExtent;
vkCmdSetScissor(m_vkCommandBuffers[m_currentFrame], 0, 1, &scissor);
}
void Device::SetViewport(SRect rect)
{
VkViewport viewport {};
viewport.x = static_cast<float>(rect.offset.x);
viewport.y = static_cast<float>(rect.offset.y);
viewport.width = static_cast<float>(rect.size.x);
viewport.height = static_cast<float>(rect.size.y);
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
vkCmdSetViewport(m_vkCommandBuffers[m_currentFrame], 0, 1, &viewport);
VkRect2D scissor {};
scissor.offset = {rect.offset.x, rect.offset.y};
scissor.extent = {static_cast<uint32_t>(rect.size.x), static_cast<uint32_t>(rect.size.y)};
vkCmdSetScissor(m_vkCommandBuffers[m_currentFrame], 0, 1, &scissor);
}
void Device::Submit()
{
_vkCmdEndRenderingKHR(m_instance.GetVkInstance(), m_vkCommandBuffers[m_currentFrame]);
TransitionImageLayout(
m_vkCommandBuffers[m_currentFrame],
m_vkSwapchainImages[m_currentImageIndex],
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
);
VK_CHECK(
vkEndCommandBuffer(m_vkCommandBuffers[m_currentFrame]),
"Failed to end command buffer"
);
VkSemaphore waitSemaphores[] = {m_vkImageSemaphores[m_currentFrame]};
VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
VkSemaphore signalSemaphores[] = {m_vkRenderSemaphores[m_currentFrame]};
VkSubmitInfo submitInfo {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &m_vkCommandBuffers[m_currentFrame];
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = waitSemaphores;
submitInfo.pWaitDstStageMask = waitStages;
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;
VK_CHECK(
vkQueueSubmit(m_vkGraphicsQueue, 1, &submitInfo, m_vkInFlightFences[m_currentFrame]),
"Failed to submit queue"
);
}
void Device::Present()
{
VkSemaphore signalSemaphores[] = {m_vkRenderSemaphores[m_currentFrame]};
VkSwapchainKHR swapchains[] = {m_vkSwapchain};
VkPresentInfoKHR presentInfo {};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = swapchains;
presentInfo.pImageIndices = &m_currentImageIndex;
auto presentResult = vkQueuePresentKHR(m_vkPresentQueue, &presentInfo);
if (presentResult == VK_ERROR_OUT_OF_DATE_KHR || presentResult == VK_SUBOPTIMAL_KHR
|| m_frameBufferResized)
{
m_frameBufferResized = false;
RecreateSwapchain();
}
else if (presentResult != VK_SUCCESS)
{
throw std::runtime_error("Failed to present queue");
}
m_currentFrame = (m_currentFrame + 1) % m_maxFramesInFlight;
}
void Device::WaitForGraphicsIdle()
{
vkQueueWaitIdle(m_vkGraphicsQueue);
}
VkCommandBuffer Device::GetTemporaryCommandBuffer()
{
VkCommandBufferAllocateInfo allocInfo {};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
// TODO: use separate pool
allocInfo.commandPool = m_vkCommandPool;
allocInfo.commandBufferCount = 1;
VkCommandBuffer commandBuffer {};
VK_CHECK(
vkAllocateCommandBuffers(m_vkDevice, &allocInfo, &commandBuffer),
"Failed to allocate temporary command buffer"
);
VkCommandBufferBeginInfo beginInfo {};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK_CHECK(
vkBeginCommandBuffer(commandBuffer, &beginInfo),
"Failed to begin temporary command buffer"
);
return commandBuffer;
}
void Device::SubmitTemporaryCommandBuffer(VkCommandBuffer commandBuffer)
{
VK_CHECK(vkEndCommandBuffer(commandBuffer), "Failed to end temporary command buffer");
VkSubmitInfo submitInfo {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;
VK_CHECK(
vkQueueSubmit(m_vkGraphicsQueue, 1, &submitInfo, VK_NULL_HANDLE),
"Failed to submit temporary command buffer"
);
// TODO: wait all copies with fences?
WaitForGraphicsIdle();
vkFreeCommandBuffers(m_vkDevice, m_vkCommandPool, 1, &commandBuffer);
}
void Device::RecreateSwapchain()
{
vkDeviceWaitIdle(m_vkDevice);
DestroySwapchain();
CreateSwapchain();
CreateImageViews();
}
void Device::DestroySwapchain()
{
for (auto view : m_vkSwapchainImageViews)
{
vkDestroyImageView(m_vkDevice, view, nullptr);
}
vkDestroySwapchainKHR(m_vkDevice, m_vkSwapchain, nullptr);
vkDestroyImageView(m_vkDevice, m_vkDepthImageView, nullptr);
vmaDestroyImage(g_vma, m_vkDepthImage, m_vmaDepthAllocation);
}
void Device::PickPhysicalDevice()
{
LOG_DEBUG("Picking physical device");
m_vkPhysicalDevice = VK_NULL_HANDLE;
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(m_instance.GetVkInstance(), &deviceCount, nullptr);
if (deviceCount == 0)
{
throw std::runtime_error("No GPUs found with support");
}
std::vector<VkPhysicalDevice> devices(deviceCount);
vkEnumeratePhysicalDevices(m_instance.GetVkInstance(), &deviceCount, devices.data());
for (const auto& device : devices)
{
if (IsDeviceSuitable(device))
{
m_vkPhysicalDevice = device;
break;
}
}
if (m_vkPhysicalDevice == VK_NULL_HANDLE)
{
throw std::runtime_error("Failed to find a suitable GPU for vulkan");
}
}
bool Device::IsDeviceSuitable(const VkPhysicalDevice device)
{
VkPhysicalDeviceProperties deviceProperties {};
vkGetPhysicalDeviceProperties(device, &deviceProperties);
VkPhysicalDeviceDynamicRenderingFeatures dynamicRenderingFeature {};
dynamicRenderingFeature.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES;
dynamicRenderingFeature.pNext = nullptr;
VkPhysicalDeviceFeatures2 deviceFeatures {};
deviceFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
deviceFeatures.pNext = &dynamicRenderingFeature;
vkGetPhysicalDeviceFeatures2(device, &deviceFeatures);
auto dynamicRenderingSupported = dynamicRenderingFeature.dynamicRendering == VK_TRUE;
auto featuresSupported = dynamicRenderingSupported;
auto familyIndices = FindQueueFamilies(device);
auto extensionsSupported = CheckDeviceExtensionSupport(device);
auto swapchainAdequate = false;
if (extensionsSupported)
{
auto swapchainSupport = QuerySwapchainSupport(device);
swapchainAdequate =
!swapchainSupport.formats.empty() && !swapchainSupport.presentModes.empty();
}
return deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU
&& familyIndices.IsComplete() && featuresSupported && extensionsSupported
&& swapchainAdequate;
}
QueueFamilyIndices Device::FindQueueFamilies(const VkPhysicalDevice device)
{
QueueFamilyIndices familyIndices;
uint32_t queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);
std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data());
uint32_t i = 0;
for (const auto& family : queueFamilies)
{
if (family.queueFlags & VK_QUEUE_GRAPHICS_BIT)
{
familyIndices.graphicsFamily = i;
}
VkBool32 presentSupport = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, m_instance.GetSurface(), &presentSupport);
if (presentSupport)
{
familyIndices.presentFamily = i;
}
if (familyIndices.IsComplete())
{
break;
}
i++;
}
return familyIndices;
}
bool Device::CheckDeviceExtensionSupport(const VkPhysicalDevice device)
{
uint32_t extensionCount;
vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr);
std::vector<VkExtensionProperties> availableExtensions(extensionCount);
vkEnumerateDeviceExtensionProperties(
device,
nullptr,
&extensionCount,
availableExtensions.data()
);
std::set<std::string> uniqueRequired(m_requiredExtensions.begin(), m_requiredExtensions.end());
for (const auto& extension : availableExtensions)
{
uniqueRequired.erase(extension.extensionName);
}
return uniqueRequired.empty();
}
SwapchainSupportDetails Device::QuerySwapchainSupport(const VkPhysicalDevice device)
{
SwapchainSupportDetails details {};
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(
device,
m_instance.GetSurface(),
&details.capabilities
);
uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(device, m_instance.GetSurface(), &formatCount, nullptr);
if (formatCount != 0)
{
details.formats.resize(formatCount);
vkGetPhysicalDeviceSurfaceFormatsKHR(
device,
m_instance.GetSurface(),
&formatCount,
details.formats.data()
);
}
uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(
device,
m_instance.GetSurface(),
&presentModeCount,
nullptr
);
if (presentModeCount != 0)
{
details.presentModes.resize(presentModeCount);
vkGetPhysicalDeviceSurfacePresentModesKHR(
device,
m_instance.GetSurface(),
&presentModeCount,
details.presentModes.data()
);
}
return details;
}
constexpr VkSurfaceFormatKHR Device::ChooseSwapSurfaceFormat(
const std::vector<VkSurfaceFormatKHR>& available
)
{
for (const auto& fmt : available)
{
if (fmt.format == VK_FORMAT_B8G8R8A8_SRGB
&& fmt.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR)
{
return fmt;
}
}
return available[0];
}
constexpr VkPresentModeKHR Device::ChooseSwapPresentMode(
const std::vector<VkPresentModeKHR>& available
)
{
for (const auto& mode : available)
{
if (mode == VK_PRESENT_MODE_MAILBOX_KHR)
{
return mode;
}
}
return VK_PRESENT_MODE_FIFO_KHR;
}
constexpr VkExtent2D Device::ChooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities)
{
if (capabilities.currentExtent.width != std::numeric_limits<uint32_t>::max())
{
return capabilities.currentExtent;
}
int width;
int height;
m_instance.GetFramebufferSize(&width, &height);
VkExtent2D extent = {
static_cast<uint32_t>(width),
static_cast<uint32_t>(height),
};
extent.width = std::clamp(
extent.width,
capabilities.minImageExtent.width,
capabilities.maxImageExtent.width
);
extent.height = std::clamp(
extent.height,
capabilities.minImageExtent.height,
capabilities.maxImageExtent.height
);
return extent;
}
void Device::CreateSwapchain()
{
auto support = QuerySwapchainSupport(m_vkPhysicalDevice);
auto surfaceFormat = ChooseSwapSurfaceFormat(support.formats);
auto presentMode = ChooseSwapPresentMode(support.presentModes);
auto extent = ChooseSwapExtent(support.capabilities);
auto imageCount = support.capabilities.minImageCount + 1;
if (support.capabilities.maxImageCount > 0 && imageCount > support.capabilities.maxImageCount)
{
imageCount = support.capabilities.maxImageCount;
}
VkSwapchainCreateInfoKHR createInfo {};
createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
createInfo.surface = m_instance.GetSurface();
createInfo.minImageCount = imageCount;
createInfo.imageFormat = surfaceFormat.format;
createInfo.imageColorSpace = surfaceFormat.colorSpace;
createInfo.imageExtent = extent;
createInfo.imageArrayLayers = 1;
createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
createInfo.preTransform = support.capabilities.currentTransform;
createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;
createInfo.oldSwapchain = VK_NULL_HANDLE;
auto families = FindQueueFamilies(m_vkPhysicalDevice);
uint32_t familyIndices[] = {families.graphicsFamily.value(), families.presentFamily.value()};
if (families.graphicsFamily != families.presentFamily)
{
createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
createInfo.queueFamilyIndexCount = 2;
createInfo.pQueueFamilyIndices = familyIndices;
}
else
{
createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
}
VK_CHECK(
vkCreateSwapchainKHR(m_vkDevice, &createInfo, nullptr, &m_vkSwapchain),
"Failed to create swapchain"
);
vkGetSwapchainImagesKHR(m_vkDevice, m_vkSwapchain, &imageCount, nullptr);
m_vkSwapchainImages.resize(imageCount);
vkGetSwapchainImagesKHR(m_vkDevice, m_vkSwapchain, &imageCount, m_vkSwapchainImages.data());
m_vkSwapchainImageFormat = surfaceFormat.format;
m_vkSwapchainExtent = extent;
CreateImage(
&m_vkDepthImage,
&m_vmaDepthAllocation,
VK_IMAGE_TYPE_2D,
GetDepthFormat(),
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
extent.width,
extent.height
);
}
void Device::CreateImage(
VkImage* image,
VmaAllocation* imageAllocation,
VkImageType imageType,
VkFormat format,
VkImageUsageFlags usage,
uint32_t width,
uint32_t height
)
{
VkImageCreateInfo createInfo {};
createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
createInfo.imageType = imageType;
createInfo.format = format;
createInfo.extent.width = width;
createInfo.extent.height = height;
createInfo.extent.depth = 1;
createInfo.mipLevels = 1;
createInfo.arrayLayers = 1;
createInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
createInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
createInfo.usage = usage;
createInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
createInfo.samples = VK_SAMPLE_COUNT_1_BIT;
createInfo.flags = 0;
VmaAllocationCreateInfo allocInfo {};
allocInfo.usage = VMA_MEMORY_USAGE_AUTO;
vmaCreateImage(g_vma, &createInfo, &allocInfo, image, imageAllocation, nullptr);
}
void Device::CreateImageView(
VkImage image,
VkImageView* imageView,
VkImageViewType viewType,
VkFormat format,
VkImageAspectFlags aspect
)
{
VkImageViewCreateInfo createInfo {};
createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
createInfo.image = image;
createInfo.viewType = viewType;
createInfo.format = format;
createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.subresourceRange.aspectMask = aspect;
createInfo.subresourceRange.baseMipLevel = 0;
createInfo.subresourceRange.levelCount = 1;
createInfo.subresourceRange.baseArrayLayer = 0;
createInfo.subresourceRange.layerCount = 1;
VK_CHECK(
vkCreateImageView(m_vkDevice, &createInfo, nullptr, imageView),
"Failed to create image view"
);
}
void Device::CreateImageViews()
{
m_vkSwapchainImageViews.resize(m_vkSwapchainImages.size());
for (size_t i = 0; i < m_vkSwapchainImages.size(); i++)
{
CreateImageView(
m_vkSwapchainImages[i],
&m_vkSwapchainImageViews[i],
VK_IMAGE_VIEW_TYPE_2D,
m_vkSwapchainImageFormat,
VK_IMAGE_ASPECT_COLOR_BIT
);
}
CreateImageView(
m_vkDepthImage,
&m_vkDepthImageView,
VK_IMAGE_VIEW_TYPE_2D,
GetDepthFormat(),
VK_IMAGE_ASPECT_DEPTH_BIT
);
}
void Device::CreateLogicalDevice()
{
LOG_DEBUG("Creating logical device");
QueueFamilyIndices familyIndices = FindQueueFamilies(m_vkPhysicalDevice);
std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
std::set<uint32_t> uniqueQueueFamilies = {
familyIndices.graphicsFamily.value(),
familyIndices.presentFamily.value()
};
auto queuePriority = 1.0f;
for (uint32_t familyIndex : uniqueQueueFamilies)
{
VkDeviceQueueCreateInfo queueCreateInfo {};
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = familyIndex;
queueCreateInfo.queueCount = 1;
queueCreateInfo.pQueuePriorities = &queuePriority;
queueCreateInfos.push_back(queueCreateInfo);
}
VkPhysicalDeviceDynamicRenderingFeatures dynamicRenderingFeature {};
dynamicRenderingFeature.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES;
dynamicRenderingFeature.dynamicRendering = VK_TRUE;
dynamicRenderingFeature.pNext = nullptr;
VkPhysicalDeviceFeatures2 deviceFeatures {};
deviceFeatures.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
deviceFeatures.pNext = &dynamicRenderingFeature;
VkDeviceCreateInfo deviceCreateInfo {};
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceCreateInfo.pQueueCreateInfos = queueCreateInfos.data();
deviceCreateInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
deviceCreateInfo.pEnabledFeatures = nullptr; // handled in pNext
deviceCreateInfo.pNext = &deviceFeatures;
deviceCreateInfo.enabledExtensionCount = static_cast<uint32_t>(m_requiredExtensions.size());
deviceCreateInfo.ppEnabledExtensionNames = m_requiredExtensions.data();
VK_CHECK(
vkCreateDevice(m_vkPhysicalDevice, &deviceCreateInfo, nullptr, &m_vkDevice),
"Failed to create logical device"
);
m_vkGraphicsQueueIndex = familyIndices.graphicsFamily.value();
m_vkPresentQueueIndex = familyIndices.presentFamily.value();
vkGetDeviceQueue(m_vkDevice, m_vkGraphicsQueueIndex, 0, &m_vkGraphicsQueue);
vkGetDeviceQueue(m_vkDevice, m_vkPresentQueueIndex, 0, &m_vkPresentQueue);
}
void Device::CreateCommandPool()
{
auto familyIndices = FindQueueFamilies(m_vkPhysicalDevice);
VkCommandPoolCreateInfo poolInfo {};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
poolInfo.queueFamilyIndex = familyIndices.graphicsFamily.value();
VK_CHECK(
vkCreateCommandPool(m_vkDevice, &poolInfo, nullptr, &m_vkCommandPool),
"Failed to create command pool"
);
}
void Device::CreateCommandBuffers()
{
m_vkCommandBuffers.resize(m_maxFramesInFlight);
VkCommandBufferAllocateInfo allocInfo {};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = m_vkCommandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = m_maxFramesInFlight;
VK_CHECK(
vkAllocateCommandBuffers(m_vkDevice, &allocInfo, m_vkCommandBuffers.data()),
"Failed to allocate command buffers"
);
}
void Device::CreateSyncObjects()
{
m_vkImageSemaphores.resize(m_maxFramesInFlight);
m_vkRenderSemaphores.resize(m_maxFramesInFlight);
m_vkInFlightFences.resize(m_maxFramesInFlight);
VkSemaphoreCreateInfo semaphoreInfo {};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
VkFenceCreateInfo fenceInfo {};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
for (uint32_t i = 0; i < m_maxFramesInFlight; i++)
{
VK_CHECK(
vkCreateSemaphore(m_vkDevice, &semaphoreInfo, nullptr, &m_vkImageSemaphores[i]),
"Failed to create image semaphore"
);
VK_CHECK(
vkCreateSemaphore(m_vkDevice, &semaphoreInfo, nullptr, &m_vkRenderSemaphores[i]),
"Failed to create render semaphore"
);
VK_CHECK(
vkCreateFence(m_vkDevice, &fenceInfo, nullptr, &m_vkInFlightFences[i]),
"Failed to create in flight fence"
);
}
}
void Device::CreateDescriptorPools()
{
VkDescriptorPoolSize uboPoolSize {};
uboPoolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
uboPoolSize.descriptorCount = m_maxFramesInFlight;
VkDescriptorPoolCreateInfo uboPoolInfo {};
uboPoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
uboPoolInfo.poolSizeCount = 1;
uboPoolInfo.pPoolSizes = &uboPoolSize;
uboPoolInfo.maxSets = m_maxFramesInFlight;
VK_CHECK(
vkCreateDescriptorPool(m_vkDevice, &uboPoolInfo, nullptr, &m_vkUboDescriptorPool),
"Failed to create ubo descriptor pool"
);
VkDescriptorPoolSize imGuiPoolSize {};
imGuiPoolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
imGuiPoolSize.descriptorCount = m_maxFramesInFlight;
VkDescriptorPoolCreateInfo imGuiPoolInfo {};
imGuiPoolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
imGuiPoolInfo.poolSizeCount = 1;
imGuiPoolInfo.pPoolSizes = &imGuiPoolSize;
imGuiPoolInfo.maxSets = m_maxFramesInFlight;
imGuiPoolInfo.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
VK_CHECK(
vkCreateDescriptorPool(m_vkDevice, &imGuiPoolInfo, nullptr, &m_vkImGuiDescriptorPool),
"Failed to create ImGui descriptor pool"
);
}
} // namespace legs

233
src/renderer/instance.cpp Normal file
View file

@ -0,0 +1,233 @@
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <legs/log.hpp>
#include <legs/renderer/common.hpp>
#include <legs/renderer/instance.hpp>
namespace legs
{
constexpr static VKAPI_ATTR VkBool32 VKAPI_CALL _VkDebugMessengerCallback(
VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
VkDebugUtilsMessageTypeFlagsEXT /*messageType*/,
const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData,
void* /*pUserData*/
)
{
auto level = LogLevel::Debug;
switch (messageSeverity)
{
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT:
level = LogLevel::Debug;
break;
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT:
level = LogLevel::Info;
break;
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT:
level = LogLevel::Warn;
break;
default:
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT:
level = LogLevel::Error;
break;
}
_LOG(level, "validation layer: {}", pCallbackData->pMessage);
return VK_FALSE;
}
constexpr static VkResult _CreateDebugUtilsMessengerEXT(
VkInstance instance,
const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkDebugUtilsMessengerEXT* pDebugMessenger
)
{
auto func = (PFN_vkCreateDebugUtilsMessengerEXT
)vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT");
if (func != nullptr)
{
return func(instance, pCreateInfo, pAllocator, pDebugMessenger);
}
else
{
return VK_ERROR_EXTENSION_NOT_PRESENT;
}
}
constexpr static void _DestroyDebugUtilsMessengerEXT(
VkInstance instance,
VkDebugUtilsMessengerEXT debugMessenger,
const VkAllocationCallbacks* pAllocator
)
{
auto func = (PFN_vkDestroyDebugUtilsMessengerEXT
)vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT");
if (func != nullptr)
{
func(instance, debugMessenger, pAllocator);
}
}
Instance::Instance(std::shared_ptr<Window> window) : m_window(window)
{
LOG_DEBUG("Creating Instance");
VkApplicationInfo appInfo {};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "legs";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "legs";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_3;
if (m_enableValidationLayers && !ValidationLayersSupported())
{
throw std::runtime_error("Validation layers not available");
}
auto extensions = GetRequiredExtensions();
VkInstanceCreateInfo createInfo {};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
createInfo.ppEnabledExtensionNames = extensions.data();
VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo;
if (m_enableValidationLayers)
{
createInfo.enabledLayerCount = static_cast<uint32_t>(m_validationLayers.size());
createInfo.ppEnabledLayerNames = m_validationLayers.data();
PopulateDebugMessengerCreateInfo(debugCreateInfo);
createInfo.pNext = &debugCreateInfo;
}
else
{
createInfo.enabledLayerCount = 0;
createInfo.pNext = nullptr;
}
VK_CHECK(vkCreateInstance(&createInfo, nullptr, &m_vkInstance), "Failed to create instance");
if (m_enableValidationLayers)
{
SetupDebugMessenger();
}
CreateSurface();
}
Instance::~Instance()
{
LOG_DEBUG("Destroying Instance");
vkDestroySurfaceKHR(m_vkInstance, m_vkSurface, nullptr);
if (m_enableValidationLayers)
{
_DestroyDebugUtilsMessengerEXT(m_vkInstance, m_vkDebugMessenger, nullptr);
}
vkDestroyInstance(m_vkInstance, nullptr);
}
void Instance::SetWindow(std::shared_ptr<Window> window)
{
m_window = window;
vkDestroySurfaceKHR(m_vkInstance, m_vkSurface, nullptr);
CreateSurface();
}
std::vector<const char*> Instance::GetRequiredExtensions()
{
unsigned int extensionCount = 0;
if (!m_window->GetExtensions(&extensionCount, nullptr))
{
throw std::runtime_error("Failed to get vulkan extensions from window");
}
std::vector<const char*> extensions(extensionCount);
if (!m_window->GetExtensions(&extensionCount, extensions.data()))
{
throw std::runtime_error("Failed to get vulkan extensions from window");
}
if (m_enableValidationLayers)
{
extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
}
return extensions;
}
bool Instance::ValidationLayersSupported()
{
uint32_t layerCount;
vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
std::vector<VkLayerProperties> availableLayers(layerCount);
vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());
for (const auto* layerName : m_validationLayers)
{
auto layerFound = false;
for (const auto& layerProperties : availableLayers)
{
if (strcmp(layerName, layerProperties.layerName) == 0)
{
layerFound = true;
break;
}
}
if (!layerFound)
{
return false;
}
}
return true;
}
void constexpr Instance::PopulateDebugMessengerCreateInfo(
VkDebugUtilsMessengerCreateInfoEXT& createInfo
)
{
createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT;
createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT
| VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT
| VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT
| VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT;
createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT
| VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT
| VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT;
createInfo.pfnUserCallback = _VkDebugMessengerCallback;
createInfo.pUserData = nullptr;
createInfo.flags = 0;
createInfo.pNext = nullptr;
}
void Instance::SetupDebugMessenger()
{
VkDebugUtilsMessengerCreateInfoEXT createInfo {};
PopulateDebugMessengerCreateInfo(createInfo);
VK_CHECK(
_CreateDebugUtilsMessengerEXT(m_vkInstance, &createInfo, nullptr, &m_vkDebugMessenger),
"Failed to set up debug messenger"
);
}
void Instance::CreateSurface()
{
auto result = m_window->CreateSurface(m_vkInstance, &m_vkSurface);
if (!result)
{
throw std::runtime_error("Failed to create vulkan surface");
}
}
} // namespace legs

205
src/renderer/renderer.cpp Normal file
View file

@ -0,0 +1,205 @@
#include <imgui_impl_vulkan.h>
#include <memory>
#include <legs/log.hpp>
#include <legs/renderer/mesh_data.hpp>
#include <legs/renderer/renderer.hpp>
#include <legs/renderer/shader.hpp>
namespace legs
{
#define MAX_FRAMES_IN_FLIGHT 2
Renderer::Renderer(std::shared_ptr<Window> window) :
m_instance(window),
m_device(m_instance, MAX_FRAMES_IN_FLIGHT)
{
LOG_INFO("Creating Renderer");
auto uboBuffers = std::vector<std::shared_ptr<Buffer>>();
for (unsigned int i = 0; i < MAX_FRAMES_IN_FLIGHT; i++)
{
uboBuffers.push_back(
std::make_shared<Buffer>(UniformBuffer, HostBuffer, sizeof(UniformBufferObject), 1)
);
}
m_descriptorSet = std::make_shared<DescriptorSet>(m_device, uboBuffers);
// TODO: abstract away all the shader + pipeline setup
auto moduleSimpleFrag = CreateShaderModule(LOAD_VULKAN_SPV(unlit_pc_frag));
auto moduleSimpleVert = CreateShaderModule(LOAD_VULKAN_SPV(unlit_pc_vert));
auto stageSimpleFrag =
FillShaderStageCreateInfo(moduleSimpleFrag, VK_SHADER_STAGE_FRAGMENT_BIT);
auto stageSimpleVert = FillShaderStageCreateInfo(moduleSimpleVert, VK_SHADER_STAGE_VERTEX_BIT);
std::vector simpleStages {stageSimpleFrag, stageSimpleVert};
m_testPipeline =
std::make_shared<Pipeline<Vertex_P_C>>(m_device, m_descriptorSet, simpleStages);
auto moduleGeoPNCFrag = CreateShaderModule(LOAD_VULKAN_SPV(lit_pnc_frag));
auto moduleGeoPNCVert = CreateShaderModule(LOAD_VULKAN_SPV(lit_pnc_vert));
auto stageGeoPNCFrag =
FillShaderStageCreateInfo(moduleGeoPNCFrag, VK_SHADER_STAGE_FRAGMENT_BIT);
auto stageGeoPNCVert = FillShaderStageCreateInfo(moduleGeoPNCVert, VK_SHADER_STAGE_VERTEX_BIT);
std::vector geoPNCStages {stageGeoPNCFrag, stageGeoPNCVert};
m_geoPNCPipeline =
std::make_shared<Pipeline<Vertex_P_N_C>>(m_device, m_descriptorSet, geoPNCStages);
auto moduleFullscreenFrag = CreateShaderModule(LOAD_VULKAN_SPV(fullscreen_frag));
auto moduleFullscreenVert = CreateShaderModule(LOAD_VULKAN_SPV(fullscreen_vert));
auto stageFullscreenFrag =
FillShaderStageCreateInfo(moduleFullscreenFrag, VK_SHADER_STAGE_FRAGMENT_BIT);
auto stageFullscreenVert =
FillShaderStageCreateInfo(moduleFullscreenVert, VK_SHADER_STAGE_VERTEX_BIT);
std::vector fullscreenStages {stageFullscreenFrag, stageFullscreenVert};
m_fullscreenPipeline = std::make_shared<Pipeline<VertexEmpty>>(
m_device,
m_descriptorSet,
fullscreenStages,
false,
false
);
auto moduleSkyFrag = CreateShaderModule(LOAD_VULKAN_SPV(sky_frag));
auto moduleSkyVert = CreateShaderModule(LOAD_VULKAN_SPV(sky_vert));
auto stageSkyFrag = FillShaderStageCreateInfo(moduleSkyFrag, VK_SHADER_STAGE_FRAGMENT_BIT);
auto stageSkyVert = FillShaderStageCreateInfo(moduleSkyVert, VK_SHADER_STAGE_VERTEX_BIT);
std::vector skyStages {stageSkyFrag, stageSkyVert};
m_skyPipeline = std::make_shared<Pipeline<Vertex_P>>(m_device, m_descriptorSet, skyStages);
m_ubo = std::make_shared<UniformBufferObject>();
}
Renderer::~Renderer()
{
LOG_INFO("Destroying Renderer");
vkDeviceWaitIdle(m_device.GetVkDevice());
m_testPipeline.reset();
m_geoPNCPipeline.reset();
m_fullscreenPipeline.reset();
m_skyPipeline.reset();
m_descriptorSet.reset();
for (auto& module : m_vkShaderModules)
{
vkDestroyShaderModule(m_device.GetVkDevice(), module, nullptr);
}
m_frameBuffers.clear();
}
void Renderer::SetWindow(std::shared_ptr<Window> window)
{
LOG_INFO("Setting window");
m_instance.SetWindow(window);
Resize();
}
void Renderer::ClearViewport()
{
auto commandBuffer = m_device.GetCommandBuffer();
BindPipeline(RenderPipeline::FULLSCREEN);
vkCmdDraw(commandBuffer, 3, 1, 0, 0);
}
void Renderer::Resize()
{
LOG_INFO("Resizing");
m_device.ResizeFramebuffer();
}
float Renderer::GetAspect()
{
return m_device.GetSwapchainAspect();
}
void Renderer::Begin()
{
m_device.Begin();
}
void Renderer::Submit()
{
m_device.Submit();
// Not ideal but guarantees chunk buffers aren't freed too early.
m_device.WaitForGraphicsIdle();
m_frameBuffers.clear();
}
void Renderer::Present()
{
m_device.Present();
}
void Renderer::UpdateUBO()
{
auto currentFrame = m_device.GetCurrentFrame();
m_descriptorSet->UpdateUBO(currentFrame, m_ubo);
}
void Renderer::WaitForIdle()
{
vkDeviceWaitIdle(m_device.GetVkDevice());
}
void Renderer::GetImGuiInfo(ImGuiCreationInfo& info)
{
info.colorFormat = m_device.GetSwapchainImageFormat();
VkFormat depthFormat = m_device.GetDepthFormat();
info.pipelineCreateInfo = {};
info.pipelineCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR;
info.pipelineCreateInfo.pNext = VK_NULL_HANDLE;
info.pipelineCreateInfo.colorAttachmentCount = 1;
info.pipelineCreateInfo.pColorAttachmentFormats = &info.colorFormat;
info.pipelineCreateInfo.depthAttachmentFormat = depthFormat;
info.pipelineCreateInfo.stencilAttachmentFormat = VK_FORMAT_UNDEFINED;
info.imGuiInfo = {};
info.imGuiInfo.Instance = m_instance.GetVkInstance();
info.imGuiInfo.PhysicalDevice = m_device.GetVkPhysicalDevice();
info.imGuiInfo.Device = m_device.GetVkDevice();
info.imGuiInfo.QueueFamily = m_device.GetGraphicsQueueIndex();
info.imGuiInfo.Queue = m_device.GetGraphicsQueue();
info.imGuiInfo.PipelineCache = VK_NULL_HANDLE; // TODO
info.imGuiInfo.DescriptorPool = m_device.GetImGuiDescriptorPool();
info.imGuiInfo.UseDynamicRendering = true;
info.imGuiInfo.PipelineRenderingCreateInfo = info.pipelineCreateInfo;
info.imGuiInfo.MinImageCount = MAX_FRAMES_IN_FLIGHT;
info.imGuiInfo.ImageCount = MAX_FRAMES_IN_FLIGHT;
info.imGuiInfo.MSAASamples = VK_SAMPLE_COUNT_1_BIT;
info.imGuiInfo.Allocator = nullptr; // TODO vma?
info.imGuiInfo.CheckVkResultFn = ImGuiVkCheck;
}
VkShaderModule& Renderer::CreateShaderModule(VkShaderModuleCreateInfo createInfo)
{
VkShaderModule module;
VK_CHECK(
vkCreateShaderModule(m_device.GetVkDevice(), &createInfo, nullptr, &module),
"Failed to create shader module"
);
return m_vkShaderModules.emplace_back(module);
}
constexpr VkPipelineShaderStageCreateInfo Renderer::FillShaderStageCreateInfo(
VkShaderModule& module,
VkShaderStageFlagBits stage
)
{
VkPipelineShaderStageCreateInfo createInfo {};
createInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
createInfo.stage = stage;
createInfo.module = module;
createInfo.pName = "main";
return createInfo;
}
} // namespace legs

View file

@ -0,0 +1,33 @@
#include "vk_mem_alloc.h"
#define VMA_IMPLEMENTATION
#define VMA_VULKAN_VERSION 1003000 // 1.3
#include <legs/renderer/common.hpp>
#include <legs/renderer/vma_usage.hpp>
namespace legs
{
VmaAllocator g_vma;
void CreateAllocator(VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice device)
{
VmaAllocatorCreateInfo createInfo {};
createInfo.instance = instance;
createInfo.physicalDevice = physicalDevice;
createInfo.device = device;
createInfo.vulkanApiVersion = VK_API_VERSION_1_3;
VK_CHECK(vmaCreateAllocator(&createInfo, &g_vma), "Failed to crate vulkan allocator");
}
void DestroyAllocator()
{
vmaDestroyAllocator(g_vma);
}
VmaTotalStatistics GetAllocatorTotalStatistics()
{
VmaTotalStatistics stats {};
vmaCalculateStatistics(g_vma, &stats);
return stats;
}
} // namespace legs

108
src/ui/ui.cpp Normal file
View file

@ -0,0 +1,108 @@
#include <imgui.h>
#include <imgui_impl_sdl2.h>
#include <imgui_impl_vulkan.h>
#include <imgui_internal.h>
#include <SDL_events.h>
#include <legs/log.hpp>
#include <legs/memory.hpp>
#include <legs/ui/ui.hpp>
namespace legs
{
UI::UI(std::shared_ptr<Window> window, std::shared_ptr<Renderer> renderer) :
m_window(window),
m_renderer(renderer),
m_state({})
{
LOG_INFO("Creating UI");
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigDebugIsDebuggerPresent = true;
ImGui_ImplSDL2_InitForVulkan(m_window->GetSDLWindow());
m_renderer->GetImGuiInfo(m_info);
ImGui_ImplVulkan_Init(&m_info.imGuiInfo);
m_state.showWindow[static_cast<unsigned int>(UIWindow::DEBUG)] = true;
}
UI::~UI()
{
LOG_INFO("Destroying UI");
ImGui_ImplVulkan_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImGui::DestroyContext();
}
void UI::Render()
{
// Start new frame
ImGui_ImplVulkan_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();
DebugWindow();
DemoWindow();
// Prep data for renderer implementation
ImGui::Render();
// Pass the data to renderer
auto data = ImGui::GetDrawData();
ImGui_ImplVulkan_RenderDrawData(data, m_renderer->GetVkCommandBuffer());
}
void UI::DebugWindow()
{
if (!m_state.showWindow[static_cast<unsigned int>(UIWindow::DEBUG)])
{
return;
}
ImGui::SetNextWindowBgAlpha(0.5f);
if (ImGui::Begin(
"Debug info",
&m_state.showWindow[static_cast<unsigned int>(UIWindow::DEBUG)],
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoNav
| ImGuiWindowFlags_NoDecoration
))
{
auto fps = std::format(
"FPS: {:.0f} ({:.2f} ms)",
1.0 / Time::DeltaFrame,
Time::DeltaFrame * 1000.0
);
ImGui::Text("%s", fps.c_str());
auto ren = std::format(" Render: {:.2f} ms", Time::DeltaRender * 1000.0);
ImGui::Text("%s", ren.c_str());
auto tps =
std::format("TPS: {:.0f} ({:.2f} ms)", 1.0 / Time::DeltaTick, Time::DeltaTick * 1000.0);
ImGui::Text("%s", tps.c_str());
auto mem = std::format("MEM: {:d} MB", Memory::GetUsage() / 1024);
ImGui::Text("%s", mem.c_str());
ImGui::End();
}
}
void UI::DemoWindow()
{
if (!m_state.showWindow[static_cast<unsigned int>(UIWindow::DEMO)])
{
return;
}
ImGui::ShowDemoWindow(&m_state.showWindow[static_cast<unsigned int>(UIWindow::DEMO)]);
}
}; // namespace legs

206
src/window/window.cpp Normal file
View file

@ -0,0 +1,206 @@
#include <imgui.h>
#include <imgui_impl_sdl2.h>
#include <SDL.h>
#include <SDL_events.h>
#include <SDL_mouse.h>
#include <SDL_scancode.h>
#include <SDL_stdinc.h>
#include <SDL_video.h>
#include <SDL_vulkan.h>
#include <legs/log.hpp>
#include "legs/window/window.hpp"
namespace legs
{
Window::Window(std::shared_ptr<InputSettings> inputSettings) : m_inputSettings(inputSettings)
{
LOG_INFO(
"Creating window, SDL version: {}.{}.{}",
SDL_MAJOR_VERSION,
SDL_MINOR_VERSION,
SDL_PATCHLEVEL
);
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
LOG_ERROR("SDL could not initialize. SDL_Error: {}", SDL_GetError());
throw("Failed to create Engine");
}
m_window = SDL_CreateWindow(
"legs",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
1280,
720,
SDL_WINDOW_VULKAN | SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_MAXIMIZED
);
if (m_window == nullptr)
{
LOG_ERROR("Window could not be created. SDL_Error: {}", SDL_GetError());
throw("Failed to create window");
}
SDL_SysWMinfo wmi;
SDL_VERSION(&wmi.version);
if (!SDL_GetWindowWMInfo(m_window, &wmi))
{
LOG_ERROR("SDL_SysWMinfo could not be retrieved. SDL_Error: {}", SDL_GetError());
throw("Failed to create window");
}
LOG_DEBUG("Window created");
}
Window::~Window()
{
LOG_INFO("Destroying window");
if (m_window != nullptr)
{
SDL_DestroyWindow(m_window);
}
SDL_Quit();
}
void Window::SetTitle(const char* title)
{
SDL_SetWindowTitle(m_window, title);
}
void Window::SetMouseGrab(bool grab)
{
SDL_SetRelativeMouseMode(grab ? SDL_TRUE : SDL_FALSE);
}
bool Window::IsMouseGrabbed()
{
return SDL_GetRelativeMouseMode() == SDL_TRUE;
}
void Window::AggregateInput(WindowInput& input)
{
ImGuiIO& io = ImGui::GetIO();
bool handleKeyboard = true;
bool handleMouse = true;
for (SDL_Event event; SDL_PollEvent(&event) != 0;)
{
if (event.type == SDL_QUIT)
{
input.wantsQuit = true;
break;
}
ImGui_ImplSDL2_ProcessEvent(&event);
handleKeyboard = !io.WantCaptureKeyboard;
handleMouse = !io.WantCaptureMouse;
if (!handleMouse)
{
SetMouseGrab(false);
}
if (!IsMouseGrabbed())
{
handleKeyboard = false;
}
if (event.type == SDL_WINDOWEVENT)
{
switch (event.window.event)
{
case SDL_WINDOWEVENT_SIZE_CHANGED:
input.wantsResize = true;
break;
default:
break;
}
}
if (handleKeyboard)
{
if (event.type == SDL_KEYDOWN)
{
auto key = m_inputSettings->GetKeyFromSDL(event.key.keysym.scancode);
input.KeyDown(key);
}
if (event.type == SDL_KEYUP)
{
auto key = m_inputSettings->GetKeyFromSDL(event.key.keysym.scancode);
input.KeyUp(key);
}
}
if (handleMouse)
{
if (event.type == SDL_MOUSEBUTTONDOWN)
{
switch (event.button.button)
{
case 1:
SetMouseGrab(true);
break;
default:
break;
}
}
if (IsMouseGrabbed())
{
if (event.type == SDL_MOUSEMOTION)
{
input.mouse.x += event.motion.xrel;
input.mouse.y += event.motion.yrel;
}
if (event.type == SDL_MOUSEWHEEL)
{
input.scroll.x += event.motion.xrel;
input.scroll.y += event.motion.yrel;
}
}
}
}
}
void Window::GetFramebufferSize(int* width, int* height)
{
SDL_Vulkan_GetDrawableSize(m_window, width, height);
}
bool Window::CreateSurface(VkInstance instance, VkSurfaceKHR* surface)
{
auto result = SDL_Vulkan_CreateSurface(m_window, instance, surface);
return result == SDL_TRUE;
}
bool Window::GetExtensions(unsigned int* pCount, const char** pNames)
{
auto result = SDL_Vulkan_GetInstanceExtensions(m_window, pCount, pNames);
return result == SDL_TRUE;
}
bool Window::IsMinimized()
{
return SDL_GetWindowFlags(m_window) & SDL_WINDOW_MINIMIZED;
}
unsigned int Window::GetRefreshRate()
{
auto display = SDL_GetWindowDisplayIndex(m_window);
if (display >= 0)
{
SDL_DisplayMode mode = {};
if (SDL_GetDesktopDisplayMode(0, &mode) == 0)
{
return static_cast<unsigned int>(mode.refresh_rate);
}
}
LOG_ERROR("Failed to get display mode, {}", SDL_GetError());
return 60;
}
} // namespace legs

85
src/world/world.cpp Normal file
View file

@ -0,0 +1,85 @@
#include <memory>
#include <glm/ext/matrix_transform.hpp>
#include "../physics.hpp"
#include <legs/entity/sky.hpp>
#include <legs/geometry/icosphere.hpp>
#include <legs/log.hpp>
#include <legs/renderer/renderer.hpp>
#include <legs/world/world.hpp>
namespace legs
{
World::World(std::shared_ptr<Renderer> renderer) :
m_renderer(renderer),
m_physics(std::make_shared<Physics>())
{
LOG_DEBUG("Creating World");
}
World::~World()
{
LOG_DEBUG("Destroying World");
m_physics.reset();
}
void World::Frame()
{
for (auto ent : m_entities)
{
ent->OnFrame();
}
}
void World::Tick()
{
{
m_physics->Update();
std::scoped_lock worldLock {m_worldMutex};
for (auto ent : m_entities)
{
ent->OnTick();
}
}
}
void World::Render()
{
if (m_sky != nullptr)
{
m_sky->Render(m_renderer);
}
for (auto ent : m_entities)
{
if (auto meshEnt = std::static_pointer_cast<MeshEntity>(ent))
{
meshEnt->Render(m_renderer);
}
}
}
void World::AddEntity(std::shared_ptr<Entity> entity)
{
m_entities.push_back(entity);
entity->OnSpawn();
}
void World::RemoveEntity(std::shared_ptr<Entity> entity)
{
for (auto it = m_entities.begin(); it != m_entities.end();)
{
if (*it == entity)
{
m_entities.erase(it);
entity->OnDestroy();
break;
}
it++;
}
}
} // namespace legs

15
subprojects/sdl2.wrap Normal file
View file

@ -0,0 +1,15 @@
[wrap-file]
directory = SDL2-2.30.6
source_url = https://github.com/libsdl-org/SDL/releases/download/release-2.30.6/SDL2-2.30.6.tar.gz
source_filename = SDL2-2.30.6.tar.gz
source_hash = c6ef64ca18a19d13df6eb22df9aff19fb0db65610a74cc81dae33a82235cacd4
patch_filename = sdl2_2.30.6-2_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/sdl2_2.30.6-2/get_patch
patch_hash = aa9f6a4947b07510c2ea84fb457e965bebe5a5deeb9f5059fbcf10dfe6b76d1f
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/sdl2_2.30.6-2/SDL2-2.30.6.tar.gz
wrapdb_version = 2.30.6-2
[provide]
sdl2 = sdl2_dep
sdl2main = sdl2main_dep
sdl2_test = sdl2_test_dep

View file

@ -0,0 +1,13 @@
[wrap-file]
directory = Vulkan-Headers-1.3.283
source_url = https://github.com/KhronosGroup/Vulkan-Headers/archive/v1.3.283.tar.gz
source_filename = vulkan-headers-1.3.283.tar.gz
source_hash = a76ff77815012c76abc9811215c2167128a73a697bcc23948e858d1f7dd54a85
patch_filename = vulkan-headers_1.3.283-1_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/vulkan-headers_1.3.283-1/get_patch
patch_hash = 00e30d35117ae90a19b5b8878746fceaf31b41778b817ca9e6b3ae6063be8233
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/vulkan-headers_1.3.283-1/vulkan-headers-1.3.283.tar.gz
wrapdb_version = 1.3.283-1
[provide]
vulkanheaders = vulkan_headers_dep

4
suppr.txt Normal file
View file

@ -0,0 +1,4 @@
# probably fine...
leak:libSDL2
leak:libubsan

4
version.h.in Normal file
View file

@ -0,0 +1,4 @@
#pragma once
#define LEGS_VERSION "@VCS_TAG@"