From 4aac4b824f6d3fd56f327d64a686d65143cff29d Mon Sep 17 00:00:00 2001 From: nullprop Date: Tue, 17 Sep 2024 19:14:58 +0300 Subject: [PATCH] Init squashed --- .clang-format | 205 ++++ .clangd | 3 + .gitattributes | 0 .gitignore | 11 + .gitmodules | 15 + LICENSE | 21 + README.md | 30 + examples/01_hello_world/main.cpp | 19 + examples/01_hello_world/meson.build | 1 + examples/02_systems/main.cpp | 163 ++++ examples/02_systems/meson.build | 1 + examples/03_physics/main.cpp | 140 +++ examples/03_physics/meson.build | 1 + examples/meson.build | 3 + include/JoltPhysics | 1 + include/VulkanMemoryAllocator | 1 + include/glm | 1 + include/imgui | 1 + meson.build | 119 +++ meson.options | 6 + scripts/compile_jolt.sh | 27 + scripts/get_steamworks_sdk.sh | 11 + scripts/run.sh | 8 + scripts/setup_debug.sh | 15 + scripts/setup_release.sh | 9 + src/engine.cpp | 290 ++++++ src/entry.cpp | 8 + src/job_system_thread_pool.cpp | 337 +++++++ src/job_system_thread_pool.hpp | 119 +++ src/job_system_with_barrier.cpp | 253 +++++ src/job_system_with_barrier.hpp | 95 ++ src/meson.build | 57 ++ src/physics.cpp | 224 +++++ src/physics.hpp | 187 ++++ src/public/legs/collider.hpp | 120 +++ src/public/legs/components/rect.hpp | 12 + src/public/legs/components/rotation.hpp | 26 + src/public/legs/components/transform.hpp | 43 + src/public/legs/engine.hpp | 99 ++ src/public/legs/entity/camera.hpp | 139 +++ src/public/legs/entity/entity.hpp | 75 ++ src/public/legs/entity/mesh_entity.hpp | 77 ++ src/public/legs/entity/physics_entity.hpp | 85 ++ src/public/legs/entity/sky.hpp | 59 ++ src/public/legs/entry.hpp | 74 ++ src/public/legs/geometry/icosphere.hpp | 195 ++++ src/public/legs/geometry/plane.hpp | 31 + src/public/legs/iphysics.hpp | 38 + src/public/legs/isystem.hpp | 20 + src/public/legs/jolt_pch.hpp | 18 + src/public/legs/log.hpp | 100 ++ src/public/legs/memory.hpp | 23 + src/public/legs/renderer/buffer.hpp | 120 +++ src/public/legs/renderer/common.hpp | 159 ++++ src/public/legs/renderer/descriptor_set.hpp | 45 + src/public/legs/renderer/device.hpp | 203 ++++ src/public/legs/renderer/instance.hpp | 59 ++ src/public/legs/renderer/mesh_data.hpp | 110 +++ src/public/legs/renderer/pipeline.hpp | 227 +++++ src/public/legs/renderer/renderer.hpp | 183 ++++ src/public/legs/renderer/shader.hpp | 41 + src/public/legs/renderer/ubo.hpp | 50 + src/public/legs/renderer/vma_usage.hpp | 14 + src/public/legs/shaders/fullscreen_frag.frag | 12 + src/public/legs/shaders/fullscreen_vert.vert | 12 + src/public/legs/shaders/include/lighting.glsl | 21 + src/public/legs/shaders/include/ubo.glsl | 17 + src/public/legs/shaders/include/util.glsl | 14 + src/public/legs/shaders/include/vertex_p.glsl | 1 + .../legs/shaders/include/vertex_pc.glsl | 2 + .../legs/shaders/include/vertex_pnc.glsl | 3 + src/public/legs/shaders/lit_pnc_frag.frag | 15 + src/public/legs/shaders/lit_pnc_vert.vert | 19 + src/public/legs/shaders/sky_frag.frag | 40 + src/public/legs/shaders/sky_vert.vert | 14 + src/public/legs/shaders/unlit_pc_frag.frag | 12 + src/public/legs/shaders/unlit_pc_vert.vert | 14 + src/public/legs/time.hpp | 108 +++ src/public/legs/ui/ui.hpp | 54 ++ src/public/legs/window/input.hpp | 130 +++ src/public/legs/window/window.hpp | 43 + src/public/legs/world/world.hpp | 58 ++ src/renderer/buffer.cpp | 253 +++++ src/renderer/descriptor_set.cpp | 116 +++ src/renderer/device.cpp | 891 ++++++++++++++++++ src/renderer/instance.cpp | 233 +++++ src/renderer/renderer.cpp | 205 ++++ src/renderer/vma_usage.cpp | 33 + src/ui/ui.cpp | 108 +++ src/window/window.cpp | 206 ++++ src/world/world.cpp | 85 ++ subprojects/sdl2.wrap | 15 + subprojects/vulkan-headers.wrap | 13 + suppr.txt | 4 + version.h.in | 4 + 95 files changed, 7582 insertions(+) create mode 100644 .clang-format create mode 100644 .clangd create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 LICENSE create mode 100644 README.md create mode 100644 examples/01_hello_world/main.cpp create mode 100644 examples/01_hello_world/meson.build create mode 100644 examples/02_systems/main.cpp create mode 100644 examples/02_systems/meson.build create mode 100644 examples/03_physics/main.cpp create mode 100644 examples/03_physics/meson.build create mode 100644 examples/meson.build create mode 160000 include/JoltPhysics create mode 160000 include/VulkanMemoryAllocator create mode 160000 include/glm create mode 160000 include/imgui create mode 100644 meson.build create mode 100644 meson.options create mode 100755 scripts/compile_jolt.sh create mode 100755 scripts/get_steamworks_sdk.sh create mode 100755 scripts/run.sh create mode 100755 scripts/setup_debug.sh create mode 100755 scripts/setup_release.sh create mode 100644 src/engine.cpp create mode 100644 src/entry.cpp create mode 100644 src/job_system_thread_pool.cpp create mode 100644 src/job_system_thread_pool.hpp create mode 100644 src/job_system_with_barrier.cpp create mode 100644 src/job_system_with_barrier.hpp create mode 100644 src/meson.build create mode 100644 src/physics.cpp create mode 100644 src/physics.hpp create mode 100644 src/public/legs/collider.hpp create mode 100644 src/public/legs/components/rect.hpp create mode 100644 src/public/legs/components/rotation.hpp create mode 100644 src/public/legs/components/transform.hpp create mode 100644 src/public/legs/engine.hpp create mode 100644 src/public/legs/entity/camera.hpp create mode 100644 src/public/legs/entity/entity.hpp create mode 100644 src/public/legs/entity/mesh_entity.hpp create mode 100644 src/public/legs/entity/physics_entity.hpp create mode 100644 src/public/legs/entity/sky.hpp create mode 100644 src/public/legs/entry.hpp create mode 100644 src/public/legs/geometry/icosphere.hpp create mode 100644 src/public/legs/geometry/plane.hpp create mode 100644 src/public/legs/iphysics.hpp create mode 100644 src/public/legs/isystem.hpp create mode 100644 src/public/legs/jolt_pch.hpp create mode 100644 src/public/legs/log.hpp create mode 100644 src/public/legs/memory.hpp create mode 100644 src/public/legs/renderer/buffer.hpp create mode 100644 src/public/legs/renderer/common.hpp create mode 100644 src/public/legs/renderer/descriptor_set.hpp create mode 100644 src/public/legs/renderer/device.hpp create mode 100644 src/public/legs/renderer/instance.hpp create mode 100644 src/public/legs/renderer/mesh_data.hpp create mode 100644 src/public/legs/renderer/pipeline.hpp create mode 100644 src/public/legs/renderer/renderer.hpp create mode 100644 src/public/legs/renderer/shader.hpp create mode 100644 src/public/legs/renderer/ubo.hpp create mode 100644 src/public/legs/renderer/vma_usage.hpp create mode 100644 src/public/legs/shaders/fullscreen_frag.frag create mode 100644 src/public/legs/shaders/fullscreen_vert.vert create mode 100644 src/public/legs/shaders/include/lighting.glsl create mode 100644 src/public/legs/shaders/include/ubo.glsl create mode 100644 src/public/legs/shaders/include/util.glsl create mode 100644 src/public/legs/shaders/include/vertex_p.glsl create mode 100644 src/public/legs/shaders/include/vertex_pc.glsl create mode 100644 src/public/legs/shaders/include/vertex_pnc.glsl create mode 100644 src/public/legs/shaders/lit_pnc_frag.frag create mode 100644 src/public/legs/shaders/lit_pnc_vert.vert create mode 100644 src/public/legs/shaders/sky_frag.frag create mode 100644 src/public/legs/shaders/sky_vert.vert create mode 100644 src/public/legs/shaders/unlit_pc_frag.frag create mode 100644 src/public/legs/shaders/unlit_pc_vert.vert create mode 100644 src/public/legs/time.hpp create mode 100644 src/public/legs/ui/ui.hpp create mode 100644 src/public/legs/window/input.hpp create mode 100644 src/public/legs/window/window.hpp create mode 100644 src/public/legs/world/world.hpp create mode 100644 src/renderer/buffer.cpp create mode 100644 src/renderer/descriptor_set.cpp create mode 100644 src/renderer/device.cpp create mode 100644 src/renderer/instance.cpp create mode 100644 src/renderer/renderer.cpp create mode 100644 src/renderer/vma_usage.cpp create mode 100644 src/ui/ui.cpp create mode 100644 src/window/window.cpp create mode 100644 src/world/world.cpp create mode 100644 subprojects/sdl2.wrap create mode 100644 subprojects/vulkan-headers.wrap create mode 100644 suppr.txt create mode 100644 version.h.in diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..7c149e6 --- /dev/null +++ b/.clang-format @@ -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 + diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..d81f1e6 --- /dev/null +++ b/.clangd @@ -0,0 +1,3 @@ +CompileFlags: + CompilationDatabase: ./build/ + diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c7932f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +subprojects/**/* +!subprojects/*.wrap + +.cache/ + +lib/ + +include/steamworks + +imgui.ini + diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a85b390 --- /dev/null +++ b/.gitmodules @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cbcd1b1 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bcf2d19 --- /dev/null +++ b/README.md @@ -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 + diff --git a/examples/01_hello_world/main.cpp b/examples/01_hello_world/main.cpp new file mode 100644 index 0000000..79a32e7 --- /dev/null +++ b/examples/01_hello_world/main.cpp @@ -0,0 +1,19 @@ +#include +#include + +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(); +} diff --git a/examples/01_hello_world/meson.build b/examples/01_hello_world/meson.build new file mode 100644 index 0000000..7484d76 --- /dev/null +++ b/examples/01_hello_world/meson.build @@ -0,0 +1 @@ +executable('01_hello_world', files('main.cpp'), dependencies: [legs_dep]) diff --git a/examples/02_systems/main.cpp b/examples/02_systems/main.cpp new file mode 100644 index 0000000..99aa7de --- /dev/null +++ b/examples/02_systems/main.cpp @@ -0,0 +1,163 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace legs; + +class MySystem : public ISystem +{ + public: + MySystem() + { + int width; + int height; + g_engine->GetWindow()->GetFramebufferSize(&width, &height); + m_camera = std::make_shared(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(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 sphereVertexBuffer; + std::shared_ptr sphereIndexBuffer; + + std::vector 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(sphereVertices.size()) + ); + renderer->CreateBuffer( + sphereIndexBuffer, + IndexBuffer, + testSphere.indices.data(), + sizeof(Index), + static_cast(testSphere.indices.size()) + ); + + auto sphere = std::make_shared(); + 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 planeVertexBuffer; + std::shared_ptr planeIndexBuffer; + + auto testPlane = SPlane({0.0f, 0.0f, 0.0f}, 20.0f); + auto planeVertices = std::array(); + 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(planeVertices.size()) + ); + renderer->CreateBuffer( + planeIndexBuffer, + IndexBuffer, + testPlane.indices.data(), + sizeof(Index), + static_cast(testPlane.indices.size()) + ); + + auto plane = std::make_shared(); + 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(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 m_camera; + std::vector> 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()); + + return LEGS_Run(); +} diff --git a/examples/02_systems/meson.build b/examples/02_systems/meson.build new file mode 100644 index 0000000..7ce5eba --- /dev/null +++ b/examples/02_systems/meson.build @@ -0,0 +1 @@ +executable('02_systems', files('main.cpp'), dependencies: [legs_dep]) diff --git a/examples/03_physics/main.cpp b/examples/03_physics/main.cpp new file mode 100644 index 0000000..e29b382 --- /dev/null +++ b/examples/03_physics/main.cpp @@ -0,0 +1,140 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include + +using namespace legs; + +class MySystem : public ISystem +{ + public: + MySystem() + { + int width; + int height; + g_engine->GetWindow()->GetFramebufferSize(&width, &height); + m_camera = std::make_shared(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 planeVertexBuffer; + std::shared_ptr planeIndexBuffer; + + auto testPlane = SPlane({0.0f, 0.0f, 0.0f}, 20.0f); + auto planeVertices = std::array(); + 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(planeVertices.size()) + ); + renderer->CreateBuffer( + planeIndexBuffer, + IndexBuffer, + testPlane.indices.data(), + sizeof(Index), + static_cast(testPlane.indices.size()) + ); + + auto plane = std::make_shared(); + 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 sphereVertexBuffer; + std::shared_ptr sphereIndexBuffer; + + std::vector 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(sphereVertices.size()) + ); + renderer->CreateBuffer( + sphereIndexBuffer, + IndexBuffer, + testSphere.indices.data(), + sizeof(Index), + static_cast(testSphere.indices.size()) + ); + + auto sphere = std::make_shared(); + 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 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()); + + return LEGS_Run(); +} diff --git a/examples/03_physics/meson.build b/examples/03_physics/meson.build new file mode 100644 index 0000000..b5eb906 --- /dev/null +++ b/examples/03_physics/meson.build @@ -0,0 +1 @@ +executable('03_physics', files('main.cpp'), dependencies: [legs_dep]) diff --git a/examples/meson.build b/examples/meson.build new file mode 100644 index 0000000..b37870e --- /dev/null +++ b/examples/meson.build @@ -0,0 +1,3 @@ +subdir('01_hello_world') +subdir('02_systems') +subdir('03_physics') diff --git a/include/JoltPhysics b/include/JoltPhysics new file mode 160000 index 0000000..cede24d --- /dev/null +++ b/include/JoltPhysics @@ -0,0 +1 @@ +Subproject commit cede24d2733a4a473c6d486650ca9b6d0481681a diff --git a/include/VulkanMemoryAllocator b/include/VulkanMemoryAllocator new file mode 160000 index 0000000..1c35ba9 --- /dev/null +++ b/include/VulkanMemoryAllocator @@ -0,0 +1 @@ +Subproject commit 1c35ba99ce775f8342d87a83a3f0f696f99c2a39 diff --git a/include/glm b/include/glm new file mode 160000 index 0000000..33b4a62 --- /dev/null +++ b/include/glm @@ -0,0 +1 @@ +Subproject commit 33b4a621a697a305bc3a7610d290677b96beb181 diff --git a/include/imgui b/include/imgui new file mode 160000 index 0000000..71714ea --- /dev/null +++ b/include/imgui @@ -0,0 +1 @@ +Subproject commit 71714eab5367d2fa119e36be30148278e0d5974f diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..dc1c347 --- /dev/null +++ b/meson.build @@ -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 diff --git a/meson.options b/meson.options new file mode 100644 index 0000000..f95eb8a --- /dev/null +++ b/meson.options @@ -0,0 +1,6 @@ +option( + 'examples', + description: 'Should the examples be built?', + type: 'boolean', + value: false +) diff --git a/scripts/compile_jolt.sh b/scripts/compile_jolt.sh new file mode 100755 index 0000000..cb98287 --- /dev/null +++ b/scripts/compile_jolt.sh @@ -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" + diff --git a/scripts/get_steamworks_sdk.sh b/scripts/get_steamworks_sdk.sh new file mode 100755 index 0000000..4e7fe6e --- /dev/null +++ b/scripts/get_steamworks_sdk.sh @@ -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 + diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100755 index 0000000..ebc40e6 --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -euo pipefail + +cd build +meson compile +LSAN_OPTIONS="suppressions=../suppr.txt" ./examples/02_systems/02_systems + diff --git a/scripts/setup_debug.sh b/scripts/setup_debug.sh new file mode 100755 index 0000000..acec974 --- /dev/null +++ b/scripts/setup_debug.sh @@ -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 + diff --git a/scripts/setup_release.sh b/scripts/setup_release.sh new file mode 100755 index 0000000..f481c1b --- /dev/null +++ b/scripts/setup_release.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -euo pipefail + +meson setup --reconfigure \ + -D examples=true \ + -D buildtype=release \ + build + diff --git a/src/engine.cpp b/src/engine.cpp new file mode 100644 index 0000000..68c4de8 --- /dev/null +++ b/src/engine.cpp @@ -0,0 +1,290 @@ +#include +#include +#include +#include + +#include + +#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(); + m_window = std::make_shared(m_inputSettings); + m_renderer = std::make_shared(m_window); + + int width; + int height; + m_window->GetFramebufferSize(&width, &height); + m_camera = std::make_shared(width, height); + + m_ui = std::make_unique(m_window, m_renderer); + + Physics::Register(); + m_world = std::make_shared(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 diff --git a/src/entry.cpp b/src/entry.cpp new file mode 100644 index 0000000..2728642 --- /dev/null +++ b/src/entry.cpp @@ -0,0 +1,8 @@ +#include + +#include + +namespace legs +{ +std::shared_ptr g_engine; +} diff --git a/src/job_system_thread_pool.cpp b/src/job_system_thread_pool.cpp new file mode 100644 index 0000000..29bbaa5 --- /dev/null +++ b/src/job_system_thread_pool.cpp @@ -0,0 +1,337 @@ +// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics) +// SPDX-FileCopyrightText: 2021 Jorrit Rouwe +// SPDX-License-Identifier: MIT + +#include + +#include +#include + +#include "job_system_thread_pool.hpp" + +#ifdef JPH_PLATFORM_LINUX +#include +#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& 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*>(JPH::Allocate(sizeof(std::atomic) * 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& 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 = 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 diff --git a/src/job_system_thread_pool.hpp b/src/job_system_thread_pool.hpp new file mode 100644 index 0000000..56e7c59 --- /dev/null +++ b/src/job_system_thread_pool.hpp @@ -0,0 +1,119 @@ +// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics) +// SPDX-FileCopyrightText: 2021 Jorrit Rouwe +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +#include + +#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 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; + AvailableJobs mJobs; + + /// Threads running jobs + JPH::Array 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 mQueue[cQueueLength]; + + // Head and tail of the queue, do this value modulo cQueueLength - 1 to get the element in the + // mQueue array + std::atomic* mHeads = nullptr; ///< Per executing thread the head of the current queue + alignas(JPH_CACHE_LINE_SIZE) std::atomic 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 mQuit = false; +}; +}; // namespace legs diff --git a/src/job_system_with_barrier.cpp b/src/job_system_with_barrier.cpp new file mode 100644 index 0000000..d168dfc --- /dev/null +++ b/src/job_system_with_barrier.cpp @@ -0,0 +1,253 @@ +// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics) +// SPDX-FileCopyrightText: 2023 Jorrit Rouwe +// SPDX-License-Identifier: MIT + +#include + +#include + +#include +#include + +#include "job_system_with_barrier.hpp" + +using namespace JPH; + +namespace legs +{ + +JobSystemWithBarrier::BarrierImpl::BarrierImpl() +{ + for (std::atomic& 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 = 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 = 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 = 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(inBarrier)->IsEmpty()); + + // Flag the barrier as unused + bool expected = true; + static_cast(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(inBarrier)->Wait(); +} + +}; // namespace legs diff --git a/src/job_system_with_barrier.hpp b/src/job_system_with_barrier.hpp new file mode 100644 index 0000000..77607bf --- /dev/null +++ b/src/job_system_with_barrier.hpp @@ -0,0 +1,95 @@ +// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics) +// SPDX-FileCopyrightText: 2023 Jorrit Rouwe +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +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 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 mJobs[cMaxJobs]; ///< List of jobs that are part of this barrier, nullptrs + ///< for empty slots + alignas(JPH_CACHE_LINE_SIZE) std::atomic 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 mJobWriteIndex {0 + }; ///< First job that can be written (modulo cMaxJobs) + std::atomic 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 diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..310e5b5 --- /dev/null +++ b/src/meson.build @@ -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], +) diff --git a/src/physics.cpp b/src/physics.cpp new file mode 100644 index 0000000..d0c7cba --- /dev/null +++ b/src/physics.cpp @@ -0,0 +1,224 @@ +#include +#include +#include +#include + +#include + +#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(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 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 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 diff --git a/src/physics.hpp b/src/physics.hpp new file mode 100644 index 0000000..5293b76 --- /dev/null +++ b/src/physics.hpp @@ -0,0 +1,187 @@ +#pragma once + +#include +#include +#include + +#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 trans) override; + void SetBodyTransform(JPH::BodyID id, std::shared_ptr 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 diff --git a/src/public/legs/collider.hpp b/src/public/legs/collider.hpp new file mode 100644 index 0000000..0ff9c4c --- /dev/null +++ b/src/public/legs/collider.hpp @@ -0,0 +1,120 @@ +#pragma once + +#include +#include + +#include + +#include + +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 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 ShapeSettings; +}; + +class BoxCollider final : public ICollider +{ + public: + BoxCollider( + JPH::EMotionType motionType, + JPH::ObjectLayer layer, + std::shared_ptr trans, + glm::vec3 size + ) + { + MotionType = motionType; + Layer = layer; + ShapeSettings = std::make_shared(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 trans, + float radius + ) + { + MotionType = motionType; + Layer = layer; + ShapeSettings = std::make_shared(radius); + CreateBody(trans); + } + + ~SphereCollider() + { + } +}; +}; // namespace legs diff --git a/src/public/legs/components/rect.hpp b/src/public/legs/components/rect.hpp new file mode 100644 index 0000000..b3cca29 --- /dev/null +++ b/src/public/legs/components/rect.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace legs +{ +struct SRect +{ + glm::ivec2 size; + glm::ivec2 offset; +}; +} // namespace legs diff --git a/src/public/legs/components/rotation.hpp b/src/public/legs/components/rotation.hpp new file mode 100644 index 0000000..0720778 --- /dev/null +++ b/src/public/legs/components/rotation.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +namespace legs +{ +struct SRotation +{ + SRotation() + { + euler = {}; + quaternion = glm::identity(); + } + + 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 diff --git a/src/public/legs/components/transform.hpp b/src/public/legs/components/transform.hpp new file mode 100644 index 0000000..25a60b0 --- /dev/null +++ b/src/public/legs/components/transform.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include + +#include + +namespace legs +{ +struct STransform +{ + glm::vec3 position; + SRotation rotation; + glm::vec3 velocity; + glm::vec3 angularVelocity; + + glm::mat4x4 GetModelMatrix() + { + auto model = glm::identity(); + 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 diff --git a/src/public/legs/engine.hpp b/src/public/legs/engine.hpp new file mode 100644 index 0000000..ea95e55 --- /dev/null +++ b/src/public/legs/engine.hpp @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace legs +{ +class Engine +{ + public: + Engine(); + ~Engine(); + + int Run(); + + std::shared_ptr GetWindow() const + { + return m_window; + } + + void AddSystem(std::shared_ptr system) + { + m_systems.push_back(system); + } + + std::shared_ptr GetRenderer() + { + return m_renderer; + } + + std::shared_ptr GetWorld() + { + return m_world; + } + + std::shared_ptr GetCamera() + { + return m_camera; + } + + void SetCamera(std::shared_ptr 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 m_inputSettings; + std::shared_ptr m_window; + std::shared_ptr m_camera; + std::shared_ptr m_renderer; + std::shared_ptr m_world; + std::unique_ptr 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> m_systems; +}; +} // namespace legs diff --git a/src/public/legs/entity/camera.hpp b/src/public/legs/entity/camera.hpp new file mode 100644 index 0000000..3fabfef --- /dev/null +++ b/src/public/legs/entity/camera.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +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(width) / static_cast(height); + viewport.x = static_cast(width); + viewport.y = static_cast(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(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(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(Time::DeltaFrame); + } + if (input.HasKey(Key::KEY_MOVE_BACK)) + { + Transform->position -= + Transform->Forward() * MoveSpeed * static_cast(Time::DeltaFrame); + } + if (input.HasKey(Key::KEY_MOVE_RIGHT)) + { + Transform->position += + Transform->Right() * MoveSpeed * static_cast(Time::DeltaFrame); + } + if (input.HasKey(Key::KEY_MOVE_LEFT)) + { + Transform->position -= + Transform->Right() * MoveSpeed * static_cast(Time::DeltaFrame); + } + if (input.HasKey(Key::KEY_MOVE_UP)) + { + Transform->position += + glm::vec3(0, 0, 1) * MoveSpeed * static_cast(Time::DeltaFrame); + } + if (input.HasKey(Key::KEY_MOVE_DOWN)) + { + Transform->position -= + glm::vec3(0, 0, 1) * MoveSpeed * static_cast(Time::DeltaFrame); + } + + UpdateMatrices(); + } + + float MoveSpeed; + float Sensitivity; +}; +} // namespace legs diff --git a/src/public/legs/entity/entity.hpp b/src/public/legs/entity/entity.hpp new file mode 100644 index 0000000..e716dca --- /dev/null +++ b/src/public/legs/entity/entity.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include +#include + +#include + +namespace legs +{ +class Entity +{ + public: + Entity() : Name(""), Transform(std::make_shared()) {}; + 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 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 Transform; +}; +}; // namespace legs diff --git a/src/public/legs/entity/mesh_entity.hpp b/src/public/legs/entity/mesh_entity.hpp new file mode 100644 index 0000000..4ee92bd --- /dev/null +++ b/src/public/legs/entity/mesh_entity.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include +#include + +#include +#include +#include + +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 vertexBuffer, + std::shared_ptr indexBuffer + ) + { + m_vertexBuffer = vertexBuffer; + m_indexBuffer = indexBuffer; + } + + virtual void Render(std::shared_ptr 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 m_vertexBuffer; + std::shared_ptr m_indexBuffer; +}; +}; // namespace legs diff --git a/src/public/legs/entity/physics_entity.hpp b/src/public/legs/entity/physics_entity.hpp new file mode 100644 index 0000000..5db86f6 --- /dev/null +++ b/src/public/legs/entity/physics_entity.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include + +#include +#include + +#include +#include + +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 diff --git a/src/public/legs/entity/sky.hpp b/src/public/legs/entity/sky.hpp new file mode 100644 index 0000000..5c5083f --- /dev/null +++ b/src/public/legs/entity/sky.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace legs +{ +class Sky : public MeshEntity +{ + public: + Sky(std::shared_ptr renderer) : MeshEntity() + { + auto icosphere = SIcosphere({}, CAM_FAR * 0.95f, 1, true); + + std::vector 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(vertices.size()) + ); + renderer->CreateBuffer( + m_indexBuffer, + IndexBuffer, + icosphere.indices.data(), + sizeof(Index), + static_cast(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) override + { + renderer->GetUBO()->sunDir = SunDirection; + renderer->GetUBO()->sunColor = SunColor; + MeshEntity::Render(renderer); + } + + glm::vec3 SunDirection; + glm::vec3 SunColor; +}; +} // namespace legs diff --git a/src/public/legs/entry.hpp b/src/public/legs/entry.hpp new file mode 100644 index 0000000..e7ea75a --- /dev/null +++ b/src/public/legs/entry.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include + +#include +#include + +namespace legs +{ +extern std::shared_ptr 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(); + 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 diff --git a/src/public/legs/geometry/icosphere.hpp b/src/public/legs/geometry/icosphere.hpp new file mode 100644 index 0000000..cfa4b4b --- /dev/null +++ b/src/public/legs/geometry/icosphere.hpp @@ -0,0 +1,195 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace legs +{ +static const glm::vec2 baseSize = {0.525731112119133606f, 0.850650808352039932f}; +static const std::array 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 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, unsigned int>; + +struct SIcosphere +{ + std::vector positions; + std::vector normals; + std::vector 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(60 * std::pow(4, subdivisions)); + auto numVerts = static_cast(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 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 diff --git a/src/public/legs/geometry/plane.hpp b/src/public/legs/geometry/plane.hpp new file mode 100644 index 0000000..01d2afd --- /dev/null +++ b/src/public/legs/geometry/plane.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include + +namespace legs +{ +static const std::array 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 planeBaseIndices = {0, 1, 2, 2, 3, 0}; + +struct SPlane +{ + std::array vertices; + std::array 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 diff --git a/src/public/legs/iphysics.hpp b/src/public/legs/iphysics.hpp new file mode 100644 index 0000000..17fdba7 --- /dev/null +++ b/src/public/legs/iphysics.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include + +#include + +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 trans) = 0; + virtual void SetBodyTransform(JPH::BodyID id, std::shared_ptr 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 diff --git a/src/public/legs/isystem.hpp b/src/public/legs/isystem.hpp new file mode 100644 index 0000000..4d7d132 --- /dev/null +++ b/src/public/legs/isystem.hpp @@ -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 diff --git a/src/public/legs/jolt_pch.hpp b/src/public/legs/jolt_pch.hpp new file mode 100644 index 0000000..f42bd73 --- /dev/null +++ b/src/public/legs/jolt_pch.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/src/public/legs/log.hpp b/src/public/legs/log.hpp new file mode 100644 index 0000000..fb19e9d --- /dev/null +++ b/src/public/legs/log.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace legs +{ +enum LogLevel +{ + Debug, + Info, + Warn, + Error, + Fatal, + MAX, +}; + +class Log +{ + public: + static void SetLogLevel(const LogLevel level) + { + m_logLevel = level; + } + + template + 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(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(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 diff --git a/src/public/legs/memory.hpp b/src/public/legs/memory.hpp new file mode 100644 index 0000000..608cc9d --- /dev/null +++ b/src/public/legs/memory.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +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 diff --git a/src/public/legs/renderer/buffer.hpp b/src/public/legs/renderer/buffer.hpp new file mode 100644 index 0000000..f86cc12 --- /dev/null +++ b/src/public/legs/renderer/buffer.hpp @@ -0,0 +1,120 @@ +#pragma once + +#include +#include + +#include + +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 + requires std::contiguous_iterator + 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(it), size); + m_elementCount = length; + } + + void Write(void* data, size_t size); + + void CopyToDevice(void* commandBuffer, std::shared_ptr 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 diff --git a/src/public/legs/renderer/common.hpp b/src/public/legs/renderer/common.hpp new file mode 100644 index 0000000..82d3ab0 --- /dev/null +++ b/src/public/legs/renderer/common.hpp @@ -0,0 +1,159 @@ +#pragma once + +#include + +#include +#include + +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 diff --git a/src/public/legs/renderer/descriptor_set.hpp b/src/public/legs/renderer/descriptor_set.hpp new file mode 100644 index 0000000..329b2f2 --- /dev/null +++ b/src/public/legs/renderer/descriptor_set.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace legs +{ +class DescriptorSet +{ + public: + DescriptorSet() = delete; + DescriptorSet(const Device& device, std::vector> 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 ubo); + + void Bind( + VkCommandBuffer commandBuffer, + VkPipelineBindPoint bindPoint, + VkPipelineLayout pipelineLayout, + uint32_t frameIndex + ); + + std::vector GetLayouts() const + { + return m_vkLayouts; + } + + private: + const Device& m_device; + std::vector m_vkLayouts; + std::vector m_vkSets; + std::vector> m_uniformBuffers; + std::vector m_ubosMappedMemory; +}; +} // namespace legs diff --git a/src/public/legs/renderer/device.hpp b/src/public/legs/renderer/device.hpp new file mode 100644 index 0000000..e0382fe --- /dev/null +++ b/src/public/legs/renderer/device.hpp @@ -0,0 +1,203 @@ +#pragma once + +#include + +#include +#include +#include + +namespace legs +{ +struct QueueFamilyIndices +{ + std::optional graphicsFamily; + std::optional presentFamily; + + bool IsComplete() const + { + return graphicsFamily.has_value() && presentFamily.has_value(); + } +}; + +struct SwapchainSupportDetails +{ + VkSurfaceCapabilitiesKHR capabilities; + std::vector formats; + std::vector 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(extent.width) / static_cast(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& available + ); + constexpr VkPresentModeKHR ChooseSwapPresentMode(const std::vector& 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 m_vkSwapchainImages; + VkFormat m_vkSwapchainImageFormat; + VkExtent2D m_vkSwapchainExtent; + std::vector m_vkSwapchainImageViews; + + VkImage m_vkDepthImage; + VmaAllocation m_vmaDepthAllocation; + VkImageView m_vkDepthImageView; + + VkCommandPool m_vkCommandPool; + std::vector m_vkCommandBuffers; + + std::vector m_vkImageSemaphores; + std::vector m_vkRenderSemaphores; + std::vector 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 m_requiredExtensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME, + VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME, + }; +}; +} // namespace legs diff --git a/src/public/legs/renderer/instance.hpp b/src/public/legs/renderer/instance.hpp new file mode 100644 index 0000000..ddf415b --- /dev/null +++ b/src/public/legs/renderer/instance.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include + +#include + +namespace legs +{ +class Instance +{ + public: + Instance(std::shared_ptr 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); + + private: + bool ValidationLayersSupported(); + std::vector GetRequiredExtensions(); + void constexpr PopulateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo); + void SetupDebugMessenger(); + void CreateSurface(); + + std::shared_ptr m_window; + + VkInstance m_vkInstance; + VkDebugUtilsMessengerEXT m_vkDebugMessenger; + VkSurfaceKHR m_vkSurface; + + const std::vector m_validationLayers = {"VK_LAYER_KHRONOS_validation"}; + +#ifdef NDEBUG + constexpr static bool m_enableValidationLayers = false; +#else + constexpr static bool m_enableValidationLayers = true; +#endif +}; +} // namespace legs diff --git a/src/public/legs/renderer/mesh_data.hpp b/src/public/legs/renderer/mesh_data.hpp new file mode 100644 index 0000000..0f3987a --- /dev/null +++ b/src/public/legs/renderer/mesh_data.hpp @@ -0,0 +1,110 @@ +#pragma once + +#include +#include + +#include + +#include + +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 +consteval VkVertexInputBindingDescription GetBindingDescription() +{ + VkVertexInputBindingDescription desc {}; + desc.binding = 0; + desc.stride = sizeof(T); + desc.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + return desc; +} + +template +constexpr std::vector GetAttributeDescriptions() +{ + std::vector desc {}; + + // TODO: reflection + + if constexpr (std::is_same_v) + { + desc.push_back({ + .location = 0, + .binding = 0, + .format = VK_FORMAT_R32G32B32_SFLOAT, + .offset = offsetof(T, position), + }); + } + else if constexpr (std::is_same_v) + { + 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) + { + 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) + { + // nada + } + else + { + static_assert(false, "Unhandled type for attribute descriptions"); + } + + return desc; +} +} // namespace legs diff --git a/src/public/legs/renderer/pipeline.hpp b/src/public/legs/renderer/pipeline.hpp new file mode 100644 index 0000000..faa0272 --- /dev/null +++ b/src/public/legs/renderer/pipeline.hpp @@ -0,0 +1,227 @@ +#pragma once + +#include + +#include "vulkan/vulkan_core.h" + +#include +#include +#include + +namespace legs +{ +template +class Pipeline +{ + public: + Pipeline() = delete; + Pipeline( + const Device& device, + std::shared_ptr descriptorSet, + std::vector 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 m_descriptorSet; + + VkPipelineLayout m_vkPipelineLayout; + VkPipeline m_vkPipeline; +}; + +template +Pipeline::Pipeline( + const Device& device, + std::shared_ptr descriptorSet, + std::vector 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(dynamicStates.size()); + dynamicState.pDynamicStates = dynamicStates.data(); + + const auto vertexBindingDescription = GetBindingDescription(); + const auto vertexAttributeDescriptions = GetAttributeDescriptions(); + + VkPipelineVertexInputStateCreateInfo vertexInputState {}; + vertexInputState.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + vertexInputState.vertexBindingDescriptionCount = 1; + vertexInputState.vertexAttributeDescriptionCount = + static_cast(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(swapchainExtent.width); + viewport.height = static_cast(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(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(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 +Pipeline::~Pipeline() +{ + LOG_DEBUG("Destroying Pipeline"); + + vkDestroyPipeline(m_device.GetVkDevice(), m_vkPipeline, nullptr); + vkDestroyPipelineLayout(m_device.GetVkDevice(), m_vkPipelineLayout, nullptr); +} + +template +void Pipeline::Bind( + VkCommandBuffer commandBuffer, + VkPipelineBindPoint bindPoint, + uint32_t frameIndex +) +{ + vkCmdBindPipeline(commandBuffer, bindPoint, m_vkPipeline); + m_descriptorSet->Bind(commandBuffer, bindPoint, m_vkPipelineLayout, frameIndex); +} +} // namespace legs diff --git a/src/public/legs/renderer/renderer.hpp b/src/public/legs/renderer/renderer.hpp new file mode 100644 index 0000000..3f66845 --- /dev/null +++ b/src/public/legs/renderer/renderer.hpp @@ -0,0 +1,183 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace legs +{ +enum RenderPipeline +{ + INVALID, + GEO_P_C, + GEO_P_N_C, + FULLSCREEN, + SKY, +}; + +class Renderer +{ + public: + Renderer(std::shared_ptr window); + ~Renderer(); + + Renderer(const Renderer&) = delete; + Renderer(Renderer&&) = delete; + Renderer& operator=(const Renderer&) = delete; + Renderer& operator=(Renderer&&) = delete; + + void SetWindow(std::shared_ptr 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 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, + BufferType bufferType, + void* data, + uint32_t elementSize, + uint32_t elementCount + ) + { + auto hostBuffer = + std::make_shared(bufferType, HostBuffer, elementSize, elementCount); + auto deviceBuffer = + std::make_shared(bufferType, DeviceBuffer, elementSize, elementCount); + hostBuffer->Write(data, elementSize * elementCount); + auto tempBuffer = GetTemporaryCommandBuffer(); + hostBuffer->CopyToDevice(tempBuffer, static_pointer_cast(deviceBuffer)); + SubmitTemporaryCommandBuffer(tempBuffer); + buffer = static_pointer_cast(deviceBuffer); + } + + void DrawWithBuffers(std::shared_ptr vertexBuffer, std::shared_ptr 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 m_descriptorSet; + std::vector m_vkShaderModules; + + std::shared_ptr> m_testPipeline; + std::shared_ptr> m_geoPNCPipeline; + std::shared_ptr> m_fullscreenPipeline; + std::shared_ptr> m_skyPipeline; + + std::shared_ptr m_ubo; + + // Hold so we don't call Buffer destructor + // while still in use by command buffer. + std::vector> m_frameBuffers; +}; +} // namespace legs diff --git a/src/public/legs/renderer/shader.hpp b/src/public/legs/renderer/shader.hpp new file mode 100644 index 0000000..08450b2 --- /dev/null +++ b/src/public/legs/renderer/shader.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include + +// Generated files by glslang +#include +#include +#include +#include +#include +#include +#include +#include + +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(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 diff --git a/src/public/legs/renderer/ubo.hpp b/src/public/legs/renderer/ubo.hpp new file mode 100644 index 0000000..7bbaab2 --- /dev/null +++ b/src/public/legs/renderer/ubo.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +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 cam) + { + model = glm::identity(); + 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 diff --git a/src/public/legs/renderer/vma_usage.hpp b/src/public/legs/renderer/vma_usage.hpp new file mode 100644 index 0000000..8d29440 --- /dev/null +++ b/src/public/legs/renderer/vma_usage.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace legs +{ +extern void CreateAllocator(VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice device); + +extern void DestroyAllocator(); + +extern VmaTotalStatistics GetAllocatorTotalStatistics(); + +extern VmaAllocator g_vma; +} // namespace legs diff --git a/src/public/legs/shaders/fullscreen_frag.frag b/src/public/legs/shaders/fullscreen_frag.frag new file mode 100644 index 0000000..dc9f1e9 --- /dev/null +++ b/src/public/legs/shaders/fullscreen_frag.frag @@ -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); +} diff --git a/src/public/legs/shaders/fullscreen_vert.vert b/src/public/legs/shaders/fullscreen_vert.vert new file mode 100644 index 0000000..b02bedd --- /dev/null +++ b/src/public/legs/shaders/fullscreen_vert.vert @@ -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); +} diff --git a/src/public/legs/shaders/include/lighting.glsl b/src/public/legs/shaders/include/lighting.glsl new file mode 100644 index 0000000..72b4d3f --- /dev/null +++ b/src/public/legs/shaders/include/lighting.glsl @@ -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; +} diff --git a/src/public/legs/shaders/include/ubo.glsl b/src/public/legs/shaders/include/ubo.glsl new file mode 100644 index 0000000..d7df8d5 --- /dev/null +++ b/src/public/legs/shaders/include/ubo.glsl @@ -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; diff --git a/src/public/legs/shaders/include/util.glsl b/src/public/legs/shaders/include/util.glsl new file mode 100644 index 0000000..8f810f2 --- /dev/null +++ b/src/public/legs/shaders/include/util.glsl @@ -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; +} diff --git a/src/public/legs/shaders/include/vertex_p.glsl b/src/public/legs/shaders/include/vertex_p.glsl new file mode 100644 index 0000000..7b9aaa5 --- /dev/null +++ b/src/public/legs/shaders/include/vertex_p.glsl @@ -0,0 +1 @@ +layout(location = 0) in vec3 inPosition; diff --git a/src/public/legs/shaders/include/vertex_pc.glsl b/src/public/legs/shaders/include/vertex_pc.glsl new file mode 100644 index 0000000..ea746b1 --- /dev/null +++ b/src/public/legs/shaders/include/vertex_pc.glsl @@ -0,0 +1,2 @@ +layout(location = 0) in vec3 inPosition; +layout(location = 1) in vec3 inColor; diff --git a/src/public/legs/shaders/include/vertex_pnc.glsl b/src/public/legs/shaders/include/vertex_pnc.glsl new file mode 100644 index 0000000..6e28d6d --- /dev/null +++ b/src/public/legs/shaders/include/vertex_pnc.glsl @@ -0,0 +1,3 @@ +layout(location = 0) in vec3 inPosition; +layout(location = 1) in vec3 inNormal; +layout(location = 2) in vec3 inColor; diff --git a/src/public/legs/shaders/lit_pnc_frag.frag b/src/public/legs/shaders/lit_pnc_frag.frag new file mode 100644 index 0000000..95ef694 --- /dev/null +++ b/src/public/legs/shaders/lit_pnc_frag.frag @@ -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); +} diff --git a/src/public/legs/shaders/lit_pnc_vert.vert b/src/public/legs/shaders/lit_pnc_vert.vert new file mode 100644 index 0000000..59f911c --- /dev/null +++ b/src/public/legs/shaders/lit_pnc_vert.vert @@ -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; +} diff --git a/src/public/legs/shaders/sky_frag.frag b/src/public/legs/shaders/sky_frag.frag new file mode 100644 index 0000000..1095919 --- /dev/null +++ b/src/public/legs/shaders/sky_frag.frag @@ -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); +} diff --git a/src/public/legs/shaders/sky_vert.vert b/src/public/legs/shaders/sky_vert.vert new file mode 100644 index 0000000..a36bece --- /dev/null +++ b/src/public/legs/shaders/sky_vert.vert @@ -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); +} diff --git a/src/public/legs/shaders/unlit_pc_frag.frag b/src/public/legs/shaders/unlit_pc_frag.frag new file mode 100644 index 0000000..2e2898f --- /dev/null +++ b/src/public/legs/shaders/unlit_pc_frag.frag @@ -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); +} diff --git a/src/public/legs/shaders/unlit_pc_vert.vert b/src/public/legs/shaders/unlit_pc_vert.vert new file mode 100644 index 0000000..20826a0 --- /dev/null +++ b/src/public/legs/shaders/unlit_pc_vert.vert @@ -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; +} diff --git a/src/public/legs/time.hpp b/src/public/legs/time.hpp new file mode 100644 index 0000000..453a36d --- /dev/null +++ b/src/public/legs/time.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include + +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(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 diff --git a/src/public/legs/ui/ui.hpp b/src/public/legs/ui/ui.hpp new file mode 100644 index 0000000..9057586 --- /dev/null +++ b/src/public/legs/ui/ui.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include + +#include +#include + +namespace legs +{ + +enum class UIWindow +{ + DEBUG, + DEMO, + MAX +}; + +struct UIState +{ + bool showWindow[static_cast(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, std::shared_ptr renderer); + ~UI(); + + void ToggleWindow(UIWindow window) + { + auto index = static_cast(window); + m_state.showWindow[index] = !m_state.showWindow[index]; + } + + void Render(); + + private: + void DebugWindow(); + void DemoWindow(); + + std::shared_ptr m_window; + std::shared_ptr m_renderer; + ImGuiCreationInfo m_info; + UIState m_state; +}; +}; // namespace legs diff --git a/src/public/legs/window/input.hpp b/src/public/legs/window/input.hpp new file mode 100644 index 0000000..6def830 --- /dev/null +++ b/src/public/legs/window/input.hpp @@ -0,0 +1,130 @@ +#pragma once + +#include +#include + +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(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(SDL_SCANCODE_W)] = Key::KEY_MOVE_FORWARD; + m_sdlKeyMap[static_cast(SDL_SCANCODE_A)] = Key::KEY_MOVE_LEFT; + m_sdlKeyMap[static_cast(SDL_SCANCODE_S)] = Key::KEY_MOVE_BACK; + m_sdlKeyMap[static_cast(SDL_SCANCODE_D)] = Key::KEY_MOVE_RIGHT; + m_sdlKeyMap[static_cast(SDL_SCANCODE_SPACE)] = Key::KEY_MOVE_UP; + m_sdlKeyMap[static_cast(SDL_SCANCODE_LCTRL)] = Key::KEY_MOVE_DOWN; + + m_sdlKeyMap[static_cast(SDL_SCANCODE_F1)] = Key::KEY_MOUSE_GRAB; + + m_sdlKeyMap[static_cast(SDL_SCANCODE_F2)] = Key::KEY_WINDOW_DEBUG; + m_sdlKeyMap[static_cast(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 diff --git a/src/public/legs/window/window.hpp b/src/public/legs/window/window.hpp new file mode 100644 index 0000000..5abec9d --- /dev/null +++ b/src/public/legs/window/window.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include +#include + +#include + +namespace legs +{ +class Window +{ + public: + Window(std::shared_ptr 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 m_inputSettings; +}; +} // namespace legs diff --git a/src/public/legs/world/world.hpp b/src/public/legs/world/world.hpp new file mode 100644 index 0000000..302c330 --- /dev/null +++ b/src/public/legs/world/world.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include + +#include + +#include +#include + +namespace legs +{ +class World +{ + public: + World() = delete; + World(std::shared_ptr 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); + void RemoveEntity(std::shared_ptr entity); + + void SetSky(std::shared_ptr sky) + { + m_sky = sky; + } + + std::shared_ptr GetSky() + { + return m_sky; + } + + std::shared_ptr GetPhysics() + { + return m_physics; + } + + private: + std::mutex m_worldMutex; + + std::shared_ptr m_renderer; + + std::vector> m_entities; + + std::shared_ptr m_sky; + + std::shared_ptr m_physics; +}; +} // namespace legs diff --git a/src/renderer/buffer.cpp b/src/renderer/buffer.cpp new file mode 100644 index 0000000..b4e9562 --- /dev/null +++ b/src/renderer/buffer.cpp @@ -0,0 +1,253 @@ +#include + +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 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(commandBuffer); + + VkBufferCopy copyRegion {}; + copyRegion.size = requiredSize; + copyRegion.srcOffset = 0; + copyRegion.dstOffset = 0; + + auto vkDeviceBuffer = static_pointer_cast(deviceBuffer); + vkCmdCopyBuffer(vkCommandBuffer, m_vkBuffer, vkDeviceBuffer->GetVkBuffer(), 1, ©Region); + + 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(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(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(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 diff --git a/src/renderer/descriptor_set.cpp b/src/renderer/descriptor_set.cpp new file mode 100644 index 0000000..a23d747 --- /dev/null +++ b/src/renderer/descriptor_set.cpp @@ -0,0 +1,116 @@ +#include +#include + +#include +#include +#include +#include + +namespace legs +{ +DescriptorSet::DescriptorSet( + const Device& device, + std::vector> 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(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 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 diff --git a/src/renderer/device.cpp b/src/renderer/device.cpp new file mode 100644 index 0000000..40640e9 --- /dev/null +++ b/src/renderer/device.cpp @@ -0,0 +1,891 @@ +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +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(m_vkSwapchainExtent.width); + viewport.height = static_cast(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(rect.offset.x); + viewport.y = static_cast(rect.offset.y); + viewport.width = static_cast(rect.size.x); + viewport.height = static_cast(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(rect.size.x), static_cast(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 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 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 availableExtensions(extensionCount); + vkEnumerateDeviceExtensionProperties( + device, + nullptr, + &extensionCount, + availableExtensions.data() + ); + + std::set 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& 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& 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::max()) + { + return capabilities.currentExtent; + } + + int width; + int height; + m_instance.GetFramebufferSize(&width, &height); + + VkExtent2D extent = { + static_cast(width), + static_cast(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 queueCreateInfos; + std::set 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(queueCreateInfos.size()); + deviceCreateInfo.pEnabledFeatures = nullptr; // handled in pNext + deviceCreateInfo.pNext = &deviceFeatures; + deviceCreateInfo.enabledExtensionCount = static_cast(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 diff --git a/src/renderer/instance.cpp b/src/renderer/instance.cpp new file mode 100644 index 0000000..d62f6de --- /dev/null +++ b/src/renderer/instance.cpp @@ -0,0 +1,233 @@ +#include +#include +#include + +#include +#include +#include + +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) : 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(extensions.size()); + createInfo.ppEnabledExtensionNames = extensions.data(); + + VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo; + if (m_enableValidationLayers) + { + createInfo.enabledLayerCount = static_cast(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) +{ + m_window = window; + vkDestroySurfaceKHR(m_vkInstance, m_vkSurface, nullptr); + CreateSurface(); +} + +std::vector 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 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 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 diff --git a/src/renderer/renderer.cpp b/src/renderer/renderer.cpp new file mode 100644 index 0000000..15ef61d --- /dev/null +++ b/src/renderer/renderer.cpp @@ -0,0 +1,205 @@ +#include +#include + +#include +#include +#include +#include + +namespace legs +{ + +#define MAX_FRAMES_IN_FLIGHT 2 + +Renderer::Renderer(std::shared_ptr window) : + m_instance(window), + m_device(m_instance, MAX_FRAMES_IN_FLIGHT) +{ + LOG_INFO("Creating Renderer"); + + auto uboBuffers = std::vector>(); + for (unsigned int i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) + { + uboBuffers.push_back( + std::make_shared(UniformBuffer, HostBuffer, sizeof(UniformBufferObject), 1) + ); + } + m_descriptorSet = std::make_shared(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>(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>(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>( + 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>(m_device, m_descriptorSet, skyStages); + + m_ubo = std::make_shared(); +} + +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) +{ + 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 diff --git a/src/renderer/vma_usage.cpp b/src/renderer/vma_usage.cpp new file mode 100644 index 0000000..73e579d --- /dev/null +++ b/src/renderer/vma_usage.cpp @@ -0,0 +1,33 @@ +#include "vk_mem_alloc.h" +#define VMA_IMPLEMENTATION +#define VMA_VULKAN_VERSION 1003000 // 1.3 + +#include +#include + +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 diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp new file mode 100644 index 0000000..39faeef --- /dev/null +++ b/src/ui/ui.cpp @@ -0,0 +1,108 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace legs +{ + +UI::UI(std::shared_ptr window, std::shared_ptr 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(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(UIWindow::DEBUG)]) + { + return; + } + + ImGui::SetNextWindowBgAlpha(0.5f); + if (ImGui::Begin( + "Debug info", + &m_state.showWindow[static_cast(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(UIWindow::DEMO)]) + { + return; + } + + ImGui::ShowDemoWindow(&m_state.showWindow[static_cast(UIWindow::DEMO)]); +} +}; // namespace legs diff --git a/src/window/window.cpp b/src/window/window.cpp new file mode 100644 index 0000000..a5e7fb5 --- /dev/null +++ b/src/window/window.cpp @@ -0,0 +1,206 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "legs/window/window.hpp" + +namespace legs +{ +Window::Window(std::shared_ptr 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(mode.refresh_rate); + } + } + + LOG_ERROR("Failed to get display mode, {}", SDL_GetError()); + return 60; +} +} // namespace legs diff --git a/src/world/world.cpp b/src/world/world.cpp new file mode 100644 index 0000000..d6e6072 --- /dev/null +++ b/src/world/world.cpp @@ -0,0 +1,85 @@ +#include + +#include + +#include "../physics.hpp" + +#include +#include +#include +#include +#include + +namespace legs +{ + +World::World(std::shared_ptr renderer) : + m_renderer(renderer), + m_physics(std::make_shared()) +{ + 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(ent)) + { + meshEnt->Render(m_renderer); + } + } +} + +void World::AddEntity(std::shared_ptr entity) +{ + m_entities.push_back(entity); + entity->OnSpawn(); +} + +void World::RemoveEntity(std::shared_ptr entity) +{ + for (auto it = m_entities.begin(); it != m_entities.end();) + { + if (*it == entity) + { + m_entities.erase(it); + entity->OnDestroy(); + break; + } + it++; + } +} +} // namespace legs diff --git a/subprojects/sdl2.wrap b/subprojects/sdl2.wrap new file mode 100644 index 0000000..bb790fb --- /dev/null +++ b/subprojects/sdl2.wrap @@ -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 diff --git a/subprojects/vulkan-headers.wrap b/subprojects/vulkan-headers.wrap new file mode 100644 index 0000000..938818f --- /dev/null +++ b/subprojects/vulkan-headers.wrap @@ -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 diff --git a/suppr.txt b/suppr.txt new file mode 100644 index 0000000..4eb546d --- /dev/null +++ b/suppr.txt @@ -0,0 +1,4 @@ +# probably fine... +leak:libSDL2 +leak:libubsan + diff --git a/version.h.in b/version.h.in new file mode 100644 index 0000000..268926f --- /dev/null +++ b/version.h.in @@ -0,0 +1,4 @@ +#pragma once + +#define LEGS_VERSION "@VCS_TAG@" +