Compare commits

..

1 commit

Author SHA1 Message Date
7f905620d3
squash
Some checks reported errors
continuous-integration/drone/pr Build was killed
2025-09-05 11:31:45 +03:30
327 changed files with 52779 additions and 11498 deletions

View file

@ -1,5 +0,0 @@
CompileFlags:
DriverMode: cl
Add:
- /EHsc
- /std:c++latest

View file

@ -25,13 +25,13 @@ trigger:
steps: steps:
- name: unit tests - name: unit tests
image: ci:latest image: amd64_gcc_unit_tests:latest
pull: if-not-exists pull: if-not-exists
commands: commands:
- ./tools/ci/amd64/gcc/unit_tests.sh - ./tools/ci/amd64/gcc/unit_tests.sh
- name: valgrind - name: valgrind
image: ci:latest image: amd64_gcc_valgrind:latest
pull: if-not-exists pull: if-not-exists
commands: commands:
- ./tools/ci/amd64/gcc/valgrind.sh - ./tools/ci/amd64/gcc/valgrind.sh
@ -46,7 +46,7 @@ trigger:
steps: steps:
- name: code coverage - name: code coverage
image: ci:latest image: amd64_clang_coverage:latest
pull: if-not-exists pull: if-not-exists
environment: environment:
CODECOV_TOKEN: CODECOV_TOKEN:
@ -55,13 +55,13 @@ steps:
- ./tools/ci/amd64/clang/coverage.sh - ./tools/ci/amd64/clang/coverage.sh
- name: leak sanitizer - name: leak sanitizer
image: ci:latest image: amd64_clang_lsan:latest
pull: if-not-exists pull: if-not-exists
commands: commands:
- ./tools/ci/amd64/clang/lsan.sh - ./tools/ci/amd64/clang/lsan.sh
- name: memory sanitizer - name: memory sanitizer
image: ci:latest image: amd64_clang_msan:latest
pull: if-not-exists pull: if-not-exists
commands: commands:
- ./tools/ci/amd64/clang/msan.sh - ./tools/ci/amd64/clang/msan.sh
@ -76,36 +76,18 @@ trigger:
steps: steps:
- name: clang tidy - name: clang tidy
image: ci:latest image: clang_tidy:latest
pull: if-not-exists pull: if-not-exists
privileged: true privileged: true
commands: commands:
- ./tools/ci/static_analysis/clang_tidy.sh - ./tools/ci/static_analysis/clang_tidy.sh
- name: shell check
image: ci:latest
pull: if-not-exists
commands:
- ./tools/ci/static_analysis/shell_check.sh
- name: clang format - name: clang format
image: ci:latest image: clang_format:latest
pull: if-not-exists pull: if-not-exists
commands: commands:
- ./tools/ci/static_analysis/clang_format.sh - ./tools/ci/static_analysis/clang_format.sh
- name: cmake format
image: ci:latest
pull: if-not-exists
commands:
- ./tools/ci/static_analysis/cmake_format.sh
- name: shell format
image: ci:latest
pull: if-not-exists
commands:
- ./tools/ci/static_analysis/shell_format.sh
--- ---
kind: pipeline kind: pipeline
type: docker type: docker
@ -121,7 +103,11 @@ steps:
image: documentation:latest image: documentation:latest
pull: if-not-exists pull: if-not-exists
commands: commands:
- pwd
- cd docs - cd docs
- mkdir generated
- touch generated/changelogs.rst
- touch generated/api.rst
- sphinx-build -M html . . - sphinx-build -M html . .
- rm -rf /light_docs_dev/* - rm -rf /light_docs_dev/*
@ -143,6 +129,7 @@ steps:
image: documentation:latest image: documentation:latest
pull: if-not-exists pull: if-not-exists
commands: commands:
- pwd
- cd docs - cd docs
- mkdir generated - mkdir generated
- touch generated/changelogs.rst - touch generated/changelogs.rst
@ -151,4 +138,3 @@ steps:
- rm -rf /light_docs/* - rm -rf /light_docs/*
- mv ./html/* /light_docs/ - mv ./html/* /light_docs/

View file

@ -1,8 +1,42 @@
cmake_minimum_required(VERSION 3.14) cmake_minimum_required(VERSION 3.14)
project(Light) project(Light)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/tools/cmake)
include(${CMAKE_CURRENT_SOURCE_DIR}/tools/cmake/functions.cmake) include(CheckCXXSourceCompiles)
include(${CMAKE_CURRENT_SOURCE_DIR}/tools/cmake/definitions.cmake) include(${CMAKE_DIR}/functions.cmake)
include(${CMAKE_CURRENT_SOURCE_DIR}/tools/cmake/options.cmake) include(${CMAKE_DIR}/definitions.cmake)
include(${CMAKE_DIR}/dependencies.cmake)
add_option(ENABLE_TESTS "Enables the building of the test modules")
add_option(ENABLE_STATIC_ANALYSIS "Makes clang-tidy checks mandatory for compilation")
if (ENABLE_STATIC_ANALYSIS)
set(CMAKE_CXX_CLANG_TIDY "clang-tidy;--warnings-as-errors=*;--allow-no-checks")
endif ()
add_option(ENABLE_LLVM_COVERAGE "Enables the code coverage instrumentation for clang")
if(ENABLE_LLVM_COVERAGE)
if (NOT "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
message(FATAL_ERROR "ENABLE_LLVM_COVERAGE only supports the clang compiler")
endif ()
# Check for libc++
check_cxx_source_compiles("
#include <string>
#ifdef _LIBCPP_VERSION
int main() { return 0; }
#else
#error Not using libc++
#endif
" USING_LIBCXX)
if(NOT USING_LIBCXX)
message(FATAL_ERROR "ENABLE_LLVM_COVERAGE requires libc++, please compile with -stdlib=libc++")
endif()
add_compile_options(-fprofile-instr-generate -fcoverage-mapping)
add_link_options(-fprofile-instr-generate -fcoverage-mapping)
endif ()
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/modules) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/modules)
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/external/glad)

128
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
Discord: Light7734#4652.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View file

@ -1,4 +1,6 @@
# Light # Light
See docs.light7734.com for a comprehensive project documentation See docs.light7734.com for a comprehensive project documentation
###### “No great thing comes into being all at once, any more than a cluster of grapes or a fig. If you tell me, 'I want a fig,' I will answer that it needs time. Let it flower first, then put forth its fruit and then ripen. I say then, if the fig tree's fruit is not brought to perfection suddenly in a single hour, would you expect to gather the fruit of a persons mind so soon and so easily? I tell you, you must not expect it.” —Epictetus, Discourses 1.15.7-8 <!---FUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUCK
MEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!!!!!!!!-->

61
conanfile.py Normal file
View file

@ -0,0 +1,61 @@
from conan import ConanFile
from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout
import shutil
import os
import git
class LightRecipe(ConanFile):
name = "Light Engine"
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeDeps"
options = {
"enable_tests": [True, False],
"enable_llvm_coverage": [True, False],
"enable_static_analysis": [True, False],
"use_mold": [True, False],
"export_compile_commands": [True, False],
}
default_options = {
"enable_tests": True,
"enable_llvm_coverage": False,
"enable_static_analysis": False,
"use_mold": False,
"export_compile_commands": True,
}
def requirements(self):
self.requires("imgui/1.92.0-docking")
self.requires("entt/3.15.0")
self.requires("glfw/3.4")
self.requires("stb/cci.20240531")
self.requires("yaml-cpp/0.8.0")
self.requires("lz4/1.10.0")
def layout(self):
cmake_layout(self)
def generate(self):
tc = CMakeToolchain(self)
tc.variables["CMAKE_BUILD_TYPE"] = self.settings.build_type
if self.options.use_mold:
tc.cache_variables["CMAKE_LINKER_TYPE"] = "MOLD"
tc.cache_variables["CMAKE_EXPORT_COMPILE_COMMANDS"] = self.options.export_compile_commands
tc.cache_variables["ENABLE_TESTS"] = self.options.enable_tests
tc.cache_variables["ENABLE_LLVM_COVERAGE"] = self.options.enable_llvm_coverage
tc.cache_variables["ENABLE_STATIC_ANALYSIS"] = self.options.enable_static_analysis
repo = git.Repo(search_parent_directories=True)
tc.cache_variables["GIT_HASH"] = repo.head.object.hexsha
tc.generate()
def build(self):
cmake = CMake(self)
cmake.configure()
cmake.build()

Binary file not shown.

View file

@ -0,0 +1,10 @@
#version 440 core
in vec4 vso_FragmentColor;
out vec4 fso_FragmentColor;
void main()
{
fso_FragmentColor = vso_FragmentColor;
}

Binary file not shown.

View file

@ -0,0 +1,17 @@
#version 440 core
layout(location = 0) in vec4 a_Position;
layout(location = 1) in vec4 a_Color;
layout(std140, binding = 0) uniform ub_ViewProjection
{
mat4 viewProjection;
};
layout(location = 0) out vec4 vso_FragmentColor;
void main()
{
gl_Position = viewProjection * a_Position;
vso_FragmentColor = a_Color;
}

Binary file not shown.

View file

@ -0,0 +1,12 @@
#version 450 core
in vec2 vso_TexCoord;
uniform sampler2D u_Texture;
out vec4 fso_FragmentColor;
void main()
{
fso_FragmentColor = texture(u_Texture, vso_TexCoord);
}

Binary file not shown.

View file

@ -0,0 +1,19 @@
#version 450 core
layout(location = 0) in vec4 a_Position;
layout(location = 1) in vec2 a_TexCoord;
layout(std140, binding = 0) uniform ub_ViewProjection
{
mat4 u_ViewProjection;
};
layout(location = 0) out vec2 vso_TexCoord;
void main()
{
gl_Position = u_ViewProjection * a_Position;
vso_TexCoord = a_TexCoord;
}

Binary file not shown.

View file

@ -0,0 +1,14 @@
#version 450 core
in vec4 vso_Tint;
in vec2 vso_TexCoord;
uniform sampler2D u_Texture;
out vec4 fso_FragmentColor;
void main()
{
fso_FragmentColor = texture(u_Texture, vso_TexCoord) * vso_Tint;
}

Binary file not shown.

View file

@ -0,0 +1,21 @@
#version 450 core
layout(location = 0) in vec4 a_Position;
layout(location = 1) in vec4 a_Tint;
layout(location = 2) in vec2 a_TexCoord;
layout(std140, binding = 0) uniform ub_ViewProjection
{
mat4 u_ViewProjection;
};
out vec4 vso_Tint;
out vec2 vso_TexCoord;
void main()
{
gl_Position = u_ViewProjection * a_Position;
vso_Tint = a_Tint;
vso_TexCoord = a_TexCoord;
}

View file

@ -1 +0,0 @@
The quick brown fox jumps over the lazy dog

View file

@ -1,10 +0,0 @@
#version 450 core
layout(location = 0) in vec3 in_frag_color;
layout(location = 0) out vec4 out_frag_color;
void main()
{
out_frag_color = vec4(in_frag_color, 1.0);
}

Binary file not shown.

View file

@ -1,26 +0,0 @@
#version 450 core
layout(push_constant ) uniform pc {
mat4 view_projection;
};
vec3 positions[3] = vec3[](
vec3(0.0, -0.5, 0.5),
vec3(0.5, 0.5, 0.5),
vec3(-0.5, 0.5, 0.5)
);
vec3 colors[3] = vec3[](
vec3(0.0, 0.0, 0.0),
vec3(0.0, 0.0, 0.0),
vec3(0.0, 0.0, 0.0)
);
layout(location = 0) out vec3 out_frag_color;
void main()
{
gl_Position = view_projection * vec4(positions[gl_VertexIndex], 1.0);
out_frag_color = colors[gl_VertexIndex];
}

Binary file not shown.

48
default_gui_layout.ini Normal file
View file

@ -0,0 +1,48 @@
[Window][Dockspace]
Pos=0,0
Size=1595,720
Collapsed=0
[Window][Debug##Default]
ViewportPos=2078,721
ViewportId=0x9F5F46A1
Size=848,1408
Collapsed=0
[Window][Dear ImGui Demo]
Pos=836,24
Size=759,696
Collapsed=0
DockId=0x00000003,1
[Window][Hierarchy]
Pos=0,24
Size=184,696
Collapsed=0
DockId=0x00000001,0
[Window][Properties]
Pos=836,24
Size=759,696
Collapsed=0
DockId=0x00000003,0
[Window][Game]
Pos=186,24
Size=648,696
Collapsed=0
DockId=0x00000002,0
[Window][Content Browser]
ViewportPos=1359,621
ViewportId=0x371352B7
Size=1274,1296
Collapsed=0
[Docking][Data]
DockSpace ID=0x1ED03EE2 Window=0x5B816B74 Pos=516,375 Size=1595,696 Split=X
DockNode ID=0x00000006 Parent=0x1ED03EE2 SizeRef=834,696 Split=X
DockNode ID=0x00000001 Parent=0x00000006 SizeRef=184,696 Selected=0x29EABFBD
DockNode ID=0x00000002 Parent=0x00000006 SizeRef=648,696 CentralNode=1 Selected=0x26816F31
DockNode ID=0x00000003 Parent=0x1ED03EE2 SizeRef=759,696 Selected=0x199AB496

2
docs/.gitignore vendored
View file

@ -1,5 +1,3 @@
_build/ _build/
generated/ generated/
html/
xml/

View file

@ -1,86 +0,0 @@
TARGET = ./
INPUT = "../modules"
RECURSIVE = YES
PROJECT_NAME = "Light"
JAVADOC_AUTOBRIEF = YES
JAVADOC_BANNER = YES
GENERATE_XML = YES
EXTRACT_PRIVATE = NO
EXTRACT_STATIC = NO
EXTRACT_LOCAL_CLASSES = NO
HIDE_UNDOC_RELATIONS = YES
HAVE_DOT = NO
GENERATE_TODOLIST = NO
GENERATE_HTML = NO
GENERATE_DOCSET = NO
GENERATE_HTMLHELP = NO
GENERATE_CHI = NO
GENERATE_QHP = NO
GENERATE_ECLIPSEHELP = NO
GENERATE_TREEVIEW = NO
GENERATE_LATEX = NO
GENERATE_RTF = NO
GENERATE_MAN = NO
GENERATE_DOCBOOK = NO
GENERATE_AUTOGEN_DEF = NO
GENERATE_SQLITE3 = NO
GENERATE_PERLMOD = NO
GENERATE_TAGFILE = NO
GENERATE_LEGEND = NO
GENERATE_TESTLIST = NO
GENERATE_BUGLIST = NO
GENERATE_DEPRECATEDLIST= NO
FILE_PATTERNS = *.c \
*.cc \
*.cxx \
*.cxxm \
*.cpp \
*.cppm \
*.ccm \
*.c++ \
*.c++m \
*.java \
*.ii \
*.ixx \
*.ipp \
*.i++ \
*.inl \
*.idl \
*.ddl \
*.odl \
*.h \
*.hh \
*.hxx \
*.hpp \
*.h++ \
*.l \
*.cs \
*.d \
*.php \
*.php4 \
*.php5 \
*.phtml \
*.inc \
*.m \
*.markdown \
*.md \
*.mm \
*.dox \
*.py \
*.pyw \
*.f90 \
*.f95 \
*.f03 \
*.f08 \
*.f18 \
*.f \
*.for \
*.vhd \
*.vhdl \
*.ucf \
*.qsf \
*.ice

View file

@ -1,17 +0,0 @@
Application
===================================================================================================
.. toctree::
:maxdepth: 3
:caption: App
Functions
---------------------------------------------------------------------------------------------------
.. doxygenfunction:: main
Classes
---------------------------------------------------------------------------------------------------
.. doxygenclass:: lt::app::ISystem
.. doxygenstruct:: lt::app::TickInfo
.. doxygenstruct:: lt::app::TickResult

View file

@ -1,13 +0,0 @@
Renderer
===================================================================================================
.. toctree::
:maxdepth: 3
:caption: App
Classes
---------------------------------------------------------------------------------------------------
.. doxygenenum:: lt::renderer::Api
.. doxygenclass:: lt::renderer::System
.. doxygenstruct:: lt::renderer::components::Sprite

View file

@ -13,21 +13,13 @@ author = 'light7734'
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = ['breathe'] extensions = []
breathe_projects = {"Light": "./xml"}
breathe_default_project = "Light"
breathe_default_members = ()
# Tell sphinx what the primary language being documented is.
primary_domain = 'cpp'
# Tell sphinx what the pygments highlight language should be.
highlight_language = 'cpp'
templates_path = ['_templates'] templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

View file

@ -0,0 +1,68 @@
from git import Repo
import re
repo = Repo(search_parent_directories=True)
assert not repo.bare
file_path = "generated/changelog.rst"
messages = []
short_shas = []
hex_shas = []
logs = []
remote_url = "https://git.light7734.com/light7734/light/commit"
def format_log(commit_type, message, major, minor, patch, short_sha, hex_sha):
href = f"{remote_url}/{hex_sha}"
version = f"{major}.{minor}.{patch}-kitten+{short_sha}";
link = f"`{version} <{remote_url}/{hex_sha}>`__"
return f"| **{message}** ({link})"
for commit in repo.iter_commits():
messages.append(commit.summary)
short_shas.append(repo.git.rev_parse(commit.hexsha, short=5))
hex_shas.append(commit.hexsha)
ver_major = 0
ver_minor = 0
ver_patch = 0
idx = len(messages)
for message in reversed(messages):
idx = idx - 1;
commit_type = re.match("^(feat|fix|refactor|perf|build|asset|test|chore|ci|docs)", message)
if not commit_type:
continue
match commit_type.group(0):
case "feat":
ver_minor = ver_minor + 1
ver_patch = 0
case "fix":
ver_patch = ver_patch + 1
case "refactor":
ver_patch = ver_patch + 1
case "perf":
ver_patch = ver_patch + 1
case "build":
ver_patch = ver_patch + 1
case "asset":
ver_patch = ver_patch + 1
logs.append(format_log(commit_type, message, ver_major, ver_minor, ver_patch, short_shas[idx], hex_shas[idx]))
with open(file_path, "w") as f:
f.write(".. changelogs\n\n\n")
f.write("Changelogs\n")
f.write("==================================================\n\n")
f.write("KITTEN\n")
f.write("--------------------------------------------------\n\n")
for log in reversed(logs):
f.write(log + '\n')

View file

@ -23,10 +23,10 @@
guidelines/conventions.rst guidelines/conventions.rst
.. toctree:: .. toctree::
:maxdepth: 3 :maxdepth: 2
:caption: API :caption: Generated Docs
api/app.rst generated/api.rst
api/renderer.rst generated/changelog.rst

35
docs/make.bat Normal file
View file

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

3
external/.clang-tidy vendored Normal file
View file

@ -0,0 +1,3 @@
# Disable all checks in this subdirectory
Checks: '-*'

3
external/glad/CMakeLists.txt vendored Normal file
View file

@ -0,0 +1,3 @@
add_library(glad ${CMAKE_CURRENT_SOURCE_DIR}/src/gl.c)
target_include_directories(glad PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(glad PUBLIC opengl::opengl)

311
external/glad/include/KHR/khrplatform.h vendored Normal file
View file

@ -0,0 +1,311 @@
#ifndef __khrplatform_h_
#define __khrplatform_h_
/*
** Copyright (c) 2008-2018 The Khronos Group Inc.
**
** Permission is hereby granted, free of charge, to any person obtaining a
** copy of this software and/or associated documentation files (the
** "Materials"), to deal in the Materials without restriction, including
** without limitation the rights to use, copy, modify, merge, publish,
** distribute, sublicense, and/or sell copies of the Materials, and to
** permit persons to whom the Materials are 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 Materials.
**
** THE MATERIALS ARE 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
** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/
/* Khronos platform-specific types and definitions.
*
* The master copy of khrplatform.h is maintained in the Khronos EGL
* Registry repository at https://github.com/KhronosGroup/EGL-Registry
* The last semantic modification to khrplatform.h was at commit ID:
* 67a3e0864c2d75ea5287b9f3d2eb74a745936692
*
* Adopters may modify this file to suit their platform. Adopters are
* encouraged to submit platform specific modifications to the Khronos
* group so that they can be included in future versions of this file.
* Please submit changes by filing pull requests or issues on
* the EGL Registry repository linked above.
*
*
* See the Implementer's Guidelines for information about where this file
* should be located on your system and for more details of its use:
* http://www.khronos.org/registry/implementers_guide.pdf
*
* This file should be included as
* #include <KHR/khrplatform.h>
* by Khronos client API header files that use its types and defines.
*
* The types in khrplatform.h should only be used to define API-specific types.
*
* Types defined in khrplatform.h:
* khronos_int8_t signed 8 bit
* khronos_uint8_t unsigned 8 bit
* khronos_int16_t signed 16 bit
* khronos_uint16_t unsigned 16 bit
* khronos_int32_t signed 32 bit
* khronos_uint32_t unsigned 32 bit
* khronos_int64_t signed 64 bit
* khronos_uint64_t unsigned 64 bit
* khronos_intptr_t signed same number of bits as a pointer
* khronos_uintptr_t unsigned same number of bits as a pointer
* khronos_ssize_t signed size
* khronos_usize_t unsigned size
* khronos_float_t signed 32 bit floating point
* khronos_time_ns_t unsigned 64 bit time in nanoseconds
* khronos_utime_nanoseconds_t unsigned time interval or absolute time in
* nanoseconds
* khronos_stime_nanoseconds_t signed time interval in nanoseconds
* khronos_boolean_enum_t enumerated boolean type. This should
* only be used as a base type when a client API's boolean type is
* an enum. Client APIs which use an integer or other type for
* booleans cannot use this as the base type for their boolean.
*
* Tokens defined in khrplatform.h:
*
* KHRONOS_FALSE, KHRONOS_TRUE Enumerated boolean false/true values.
*
* KHRONOS_SUPPORT_INT64 is 1 if 64 bit integers are supported; otherwise 0.
* KHRONOS_SUPPORT_FLOAT is 1 if floats are supported; otherwise 0.
*
* Calling convention macros defined in this file:
* KHRONOS_APICALL
* KHRONOS_APIENTRY
* KHRONOS_APIATTRIBUTES
*
* These may be used in function prototypes as:
*
* KHRONOS_APICALL void KHRONOS_APIENTRY funcname(
* int arg1,
* int arg2) KHRONOS_APIATTRIBUTES;
*/
#if defined(__SCITECH_SNAP__) && !defined(KHRONOS_STATIC)
# define KHRONOS_STATIC 1
#endif
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APICALL
*-------------------------------------------------------------------------
* This precedes the return type of the function in the function prototype.
*/
#if defined(KHRONOS_STATIC)
/* If the preprocessor constant KHRONOS_STATIC is defined, make the
* header compatible with static linking. */
# define KHRONOS_APICALL
#elif defined(_WIN32)
# define KHRONOS_APICALL __declspec(dllimport)
#elif defined (__SYMBIAN32__)
# define KHRONOS_APICALL IMPORT_C
#elif defined(__ANDROID__)
# define KHRONOS_APICALL __attribute__((visibility("default")))
#else
# define KHRONOS_APICALL
#endif
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APIENTRY
*-------------------------------------------------------------------------
* This follows the return type of the function and precedes the function
* name in the function prototype.
*/
#if defined(_WIN32) && !defined(_WIN32_WCE) && !defined(__SCITECH_SNAP__)
/* Win32 but not WinCE */
# define KHRONOS_APIENTRY __stdcall
#else
# define KHRONOS_APIENTRY
#endif
/*-------------------------------------------------------------------------
* Definition of KHRONOS_APIATTRIBUTES
*-------------------------------------------------------------------------
* This follows the closing parenthesis of the function prototype arguments.
*/
#if defined (__ARMCC_2__)
#define KHRONOS_APIATTRIBUTES __softfp
#else
#define KHRONOS_APIATTRIBUTES
#endif
/*-------------------------------------------------------------------------
* basic type definitions
*-----------------------------------------------------------------------*/
#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__GNUC__) || defined(__SCO__) || defined(__USLC__)
/*
* Using <stdint.h>
*/
#include <stdint.h>
typedef int32_t khronos_int32_t;
typedef uint32_t khronos_uint32_t;
typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
/*
* To support platform where unsigned long cannot be used interchangeably with
* inptr_t (e.g. CHERI-extended ISAs), we can use the stdint.h intptr_t.
* Ideally, we could just use (u)intptr_t everywhere, but this could result in
* ABI breakage if khronos_uintptr_t is changed from unsigned long to
* unsigned long long or similar (this results in different C++ name mangling).
* To avoid changes for existing platforms, we restrict usage of intptr_t to
* platforms where the size of a pointer is larger than the size of long.
*/
#if defined(__SIZEOF_LONG__) && defined(__SIZEOF_POINTER__)
#if __SIZEOF_POINTER__ > __SIZEOF_LONG__
#define KHRONOS_USE_INTPTR_T
#endif
#endif
#elif defined(__VMS ) || defined(__sgi)
/*
* Using <inttypes.h>
*/
#include <inttypes.h>
typedef int32_t khronos_int32_t;
typedef uint32_t khronos_uint32_t;
typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif defined(_WIN32) && !defined(__SCITECH_SNAP__)
/*
* Win32
*/
typedef __int32 khronos_int32_t;
typedef unsigned __int32 khronos_uint32_t;
typedef __int64 khronos_int64_t;
typedef unsigned __int64 khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif defined(__sun__) || defined(__digital__)
/*
* Sun or Digital
*/
typedef int khronos_int32_t;
typedef unsigned int khronos_uint32_t;
#if defined(__arch64__) || defined(_LP64)
typedef long int khronos_int64_t;
typedef unsigned long int khronos_uint64_t;
#else
typedef long long int khronos_int64_t;
typedef unsigned long long int khronos_uint64_t;
#endif /* __arch64__ */
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#elif 0
/*
* Hypothetical platform with no float or int64 support
*/
typedef int khronos_int32_t;
typedef unsigned int khronos_uint32_t;
#define KHRONOS_SUPPORT_INT64 0
#define KHRONOS_SUPPORT_FLOAT 0
#else
/*
* Generic fallback
*/
#include <stdint.h>
typedef int32_t khronos_int32_t;
typedef uint32_t khronos_uint32_t;
typedef int64_t khronos_int64_t;
typedef uint64_t khronos_uint64_t;
#define KHRONOS_SUPPORT_INT64 1
#define KHRONOS_SUPPORT_FLOAT 1
#endif
/*
* Types that are (so far) the same on all platforms
*/
typedef signed char khronos_int8_t;
typedef unsigned char khronos_uint8_t;
typedef signed short int khronos_int16_t;
typedef unsigned short int khronos_uint16_t;
/*
* Types that differ between LLP64 and LP64 architectures - in LLP64,
* pointers are 64 bits, but 'long' is still 32 bits. Win64 appears
* to be the only LLP64 architecture in current use.
*/
#ifdef KHRONOS_USE_INTPTR_T
typedef intptr_t khronos_intptr_t;
typedef uintptr_t khronos_uintptr_t;
#elif defined(_WIN64)
typedef signed long long int khronos_intptr_t;
typedef unsigned long long int khronos_uintptr_t;
#else
typedef signed long int khronos_intptr_t;
typedef unsigned long int khronos_uintptr_t;
#endif
#if defined(_WIN64)
typedef signed long long int khronos_ssize_t;
typedef unsigned long long int khronos_usize_t;
#else
typedef signed long int khronos_ssize_t;
typedef unsigned long int khronos_usize_t;
#endif
#if KHRONOS_SUPPORT_FLOAT
/*
* Float type
*/
typedef float khronos_float_t;
#endif
#if KHRONOS_SUPPORT_INT64
/* Time types
*
* These types can be used to represent a time interval in nanoseconds or
* an absolute Unadjusted System Time. Unadjusted System Time is the number
* of nanoseconds since some arbitrary system event (e.g. since the last
* time the system booted). The Unadjusted System Time is an unsigned
* 64 bit value that wraps back to 0 every 584 years. Time intervals
* may be either signed or unsigned.
*/
typedef khronos_uint64_t khronos_utime_nanoseconds_t;
typedef khronos_int64_t khronos_stime_nanoseconds_t;
#endif
/*
* Dummy value used to pad enum types to 32 bits.
*/
#ifndef KHRONOS_MAX_ENUM
#define KHRONOS_MAX_ENUM 0x7FFFFFFF
#endif
/*
* Enumerated boolean type
*
* Values other than zero should be considered to be true. Therefore
* comparisons should not be made against KHRONOS_TRUE.
*/
typedef enum {
KHRONOS_FALSE = 0,
KHRONOS_TRUE = 1,
KHRONOS_BOOLEAN_ENUM_FORCE_SIZE = KHRONOS_MAX_ENUM
} khronos_boolean_enum_t;
#endif /* __khrplatform_h_ */

8319
external/glad/include/glad/gl.h vendored Normal file

File diff suppressed because it is too large Load diff

7841
external/glad/include/glad/vulkan.h vendored Normal file

File diff suppressed because it is too large Load diff

84
external/glad/include/vk_platform.h vendored Normal file
View file

@ -0,0 +1,84 @@
/* */
/* File: vk_platform.h */
/* */
/*
** Copyright 2014-2025 The Khronos Group Inc.
**
** SPDX-License-Identifier: Apache-2.0
*/
#ifndef VK_PLATFORM_H_
#define VK_PLATFORM_H_
#ifdef __cplusplus
extern "C"
{
#endif /* __cplusplus */
/*
***************************************************************************************************
* Platform-specific directives and type declarations
***************************************************************************************************
*/
/* Platform-specific calling convention macros.
*
* Platforms should define these so that Vulkan clients call Vulkan commands
* with the same calling conventions that the Vulkan implementation expects.
*
* VKAPI_ATTR - Placed before the return type in function declarations.
* Useful for C++11 and GCC/Clang-style function attribute syntax.
* VKAPI_CALL - Placed after the return type in function declarations.
* Useful for MSVC-style calling convention syntax.
* VKAPI_PTR - Placed between the '(' and '*' in function pointer types.
*
* Function declaration: VKAPI_ATTR void VKAPI_CALL vkCommand(void);
* Function pointer type: typedef void (VKAPI_PTR *PFN_vkCommand)(void);
*/
#if defined(_WIN32)
/* On Windows, Vulkan commands use the stdcall convention */
#define VKAPI_ATTR
#define VKAPI_CALL __stdcall
#define VKAPI_PTR VKAPI_CALL
#elif defined(__ANDROID__) && defined(__ARM_ARCH) && __ARM_ARCH < 7
#error "Vulkan is not supported for the 'armeabi' NDK ABI"
#elif defined(__ANDROID__) && defined(__ARM_ARCH) && __ARM_ARCH >= 7 && defined(__ARM_32BIT_STATE)
/* On Android 32-bit ARM targets, Vulkan functions use the "hardfloat" */
/* calling convention, i.e. float parameters are passed in registers. This */
/* is true even if the rest of the application passes floats on the stack, */
/* as it does by default when compiling for the armeabi-v7a NDK ABI. */
#define VKAPI_ATTR __attribute__((pcs("aapcs-vfp")))
#define VKAPI_CALL
#define VKAPI_PTR VKAPI_ATTR
#else
/* On other platforms, use the default calling convention */
#define VKAPI_ATTR
#define VKAPI_CALL
#define VKAPI_PTR
#endif
#if !defined(VK_NO_STDDEF_H)
#include <stddef.h>
#endif /* !defined(VK_NO_STDDEF_H) */
#if !defined(VK_NO_STDINT_H)
#if defined(_MSC_VER) && (_MSC_VER < 1600)
typedef signed __int8 int8_t;
typedef unsigned __int8 uint8_t;
typedef signed __int16 int16_t;
typedef unsigned __int16 uint16_t;
typedef signed __int32 int32_t;
typedef unsigned __int32 uint32_t;
typedef signed __int64 int64_t;
typedef unsigned __int64 uint64_t;
#else
#include <stdint.h>
#endif
#endif /* !defined(VK_NO_STDINT_H) */
#ifdef __cplusplus
} /* extern "C" */
#endif /* __cplusplus */
#endif

17892
external/glad/src/gl.c vendored Normal file

File diff suppressed because it is too large Load diff

5537
external/glad/src/vulkan.c vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,22 +1,20 @@
# engine # engine
add_subdirectory(./std) add_subdirectory(./base)
add_subdirectory(./bitwise)
add_subdirectory(./env)
add_subdirectory(./memory)
add_subdirectory(./time) add_subdirectory(./time)
add_subdirectory(./logger) add_subdirectory(./logger)
add_subdirectory(./debug) add_subdirectory(./debug)
add_subdirectory(./math) add_subdirectory(./math)
# #
add_subdirectory(./asset_baker) add_subdirectory(./asset_baker)
add_subdirectory(./assets) add_subdirectory(./asset_parser)
# add_subdirectory(./asset_manager)
# #
add_subdirectory(./camera) add_subdirectory(./camera)
add_subdirectory(./input) # add_subdirectory(./input)
# add_subdirectory(./ui) # add_subdirectory(./ui)
# #
add_subdirectory(./surface) add_subdirectory(./surface)
add_subdirectory(./renderer) # add_subdirectory(./renderer)
add_subdirectory(./ecs) add_subdirectory(./ecs)
# #
add_subdirectory(./app) add_subdirectory(./app)

View file

@ -1,5 +1,2 @@
add_library_module(app application.cpp) add_library_module(app application.cpp)
target_link_libraries( target_link_libraries(app PRIVATE lt_debug)
app
PUBLIC memory
PRIVATE lt_debug)

View file

@ -1,6 +1,5 @@
#include <app/application.hpp> #include <app/application.hpp>
#include <app/system.hpp> #include <app/system.hpp>
#include <memory/reference.hpp>
namespace lt::app { namespace lt::app {
@ -10,16 +9,10 @@ void Application::game_loop()
{ {
for (auto &system : m_systems) for (auto &system : m_systems)
{ {
const auto &last_tick = system->get_last_tick_result(); if (system->tick())
const auto now = std::chrono::steady_clock::now(); {
return;
system->tick(
TickInfo {
.delta_time = now - last_tick.end_time,
.budget = std::chrono::milliseconds { 10 },
.start_time = now,
} }
);
} }
for (auto &system : m_systems_to_be_registered) for (auto &system : m_systems_to_be_registered)
@ -42,12 +35,12 @@ void Application::game_loop()
} }
} }
void Application::register_system(memory::Ref<app::ISystem> system) void Application::register_system(Ref<app::ISystem> system)
{ {
m_systems.emplace_back(std::move(system)); m_systems.emplace_back(std::move(system));
} }
void Application::unregister_system(memory::Ref<app::ISystem> system) void Application::unregister_system(Ref<app::ISystem> system)
{ {
m_systems_to_be_unregistered.emplace_back(std::move(system)); m_systems_to_be_unregistered.emplace_back(std::move(system));
} }

View file

@ -1,13 +1,10 @@
#pragma once #pragma once
#include <memory/reference.hpp>
#include <memory/scope.hpp>
namespace lt::app { namespace lt::app {
class ISystem; class ISystem;
extern memory::Scope<class Application> create_application(); extern Scope<class Application> create_application();
/** The main application class. /** The main application class.
* Think of this like an aggregate of systems, you register systems through this interface. * Think of this like an aggregate of systems, you register systems through this interface.
@ -28,19 +25,19 @@ public:
void game_loop(); void game_loop();
void register_system(memory::Ref<app::ISystem> system); void register_system(Ref<app::ISystem> system);
void unregister_system(memory::Ref<app::ISystem> system); void unregister_system(Ref<app::ISystem> system);
protected: protected:
Application() = default; Application() = default;
private: private:
std::vector<memory::Ref<app::ISystem>> m_systems; std::vector<Ref<app::ISystem>> m_systems;
std::vector<memory::Ref<app::ISystem>> m_systems_to_be_unregistered; std::vector<Ref<app::ISystem>> m_systems_to_be_unregistered;
std::vector<memory::Ref<app::ISystem>> m_systems_to_be_registered; std::vector<Ref<app::ISystem>> m_systems_to_be_registered;
}; };

View file

@ -1,8 +1,6 @@
#pragma once #pragma once
#include <app/application.hpp> #include <app/application.hpp>
#include <logger/logger.hpp>
#include <memory/scope.hpp>
auto main(int argc, char *argv[]) -> int32_t auto main(int argc, char *argv[]) -> int32_t
try try
@ -10,7 +8,8 @@ try
std::ignore = argc; std::ignore = argc;
std::ignore = argv; std::ignore = argv;
auto application = lt::memory::Scope<lt::app::Application> {}; auto application = lt::Scope<lt::app::Application> {};
application = lt::app::create_application(); application = lt::app::create_application();
if (!application) if (!application)
{ {
@ -22,7 +21,7 @@ try
} }
catch (const std::exception &exp) catch (const std::exception &exp)
{ {
lt::log::critical("Terminating due to uncaught exception:"); log_crt("Terminating due to uncaught exception:");
lt::log::critical("\texception.what(): {}", exp.what()); log_crt("\texception.what(): {}", exp.what());
return EXIT_FAILURE; return EXIT_FAILURE;
} }

View file

@ -1,90 +1,7 @@
#pragma once #pragma once
#include <chrono>
#include <logger/logger.hpp>
namespace lt::app { namespace lt::app {
/** Information required to tick a system.
* @note May be used across an entire application-frame (consisting of multiple systems ticking)
*/
struct TickInfo
{
using Timepoint_T = std::chrono::time_point<std::chrono::steady_clock>;
using Duration_T = std::chrono::duration<double>;
/** Duration since previous tick's end_time to current tick's start_time. */
Duration_T delta_time {};
/** Maximum duration the system is expected to finish ticking in.
*
* if end_time - start_time > budget -> the system exceeded its ticking budget.
* else end_time - start_time < budget -> the system ticked properly.
*
* In other words, end_time is expected to be less than start_time + budget.
*/
Duration_T budget {};
/** Exact time which ticking started. */
Timepoint_T start_time;
};
/** Information about how a system's tick performed */
struct TickResult
{
using Timepoint_T = std::chrono::time_point<std::chrono::steady_clock>;
using Duration_T = std::chrono::duration<double>;
/** The info supplied to the system for ticking. */
TickInfo info;
/** Equivalent to end_time - info.start_time. */
Duration_T duration {};
/** Exact time which ticking ended. */
Timepoint_T end_time;
};
struct SystemDiagnosis
{
enum class Severity : uint8_t
{
verbose,
info,
warning,
error,
fatal,
};
std::string message;
std::string code;
Severity severity;
};
class SystemStats
{
public:
void push_diagnosis(SystemDiagnosis &&diagnosis)
{
auto diag = m_diagnosis.emplace_back(std::move(diagnosis));
log::debug("message: {}", diag.message);
}
[[nodiscard]] auto empty_diagnosis() const -> bool
{
return m_diagnosis.empty();
}
private:
std::vector<SystemDiagnosis> m_diagnosis;
};
class ISystem class ISystem
{ {
public: public:
@ -104,9 +21,7 @@ public:
virtual void on_unregister() = 0; virtual void on_unregister() = 0;
virtual void tick(TickInfo tick) = 0; virtual auto tick() -> bool = 0;
[[nodiscard]] virtual auto get_last_tick_result() const -> const TickResult & = 0;
}; };
} // namespace lt::app } // namespace lt::app

View file

@ -1,6 +1,10 @@
add_library_module(libasset_baker bakers.cpp) add_executable_module(
target_link_libraries(libasset_baker PUBLIC assets logger lt_debug tbb) asset_baker entrypoint/baker.cpp
add_test_module(libasset_baker bakers.test.cpp) )
add_executable_module(asset_baker entrypoint/baker.cpp) target_link_libraries(
target_link_libraries(asset_baker PRIVATE libasset_baker) asset_baker
PRIVATE asset_parser
PRIVATE stb::stb
PRIVATE logger
)

View file

@ -1,2 +0,0 @@
#include <asset_baker/bakers.hpp>
#include <test/test.hpp>

View file

@ -1,7 +1,69 @@
#include <asset_baker/bakers.hpp> #include <asset_baker/bakers.hpp>
#include <assets/shader.hpp> #include <asset_parser/assets/text.hpp>
#include <asset_parser/assets/texture.hpp>
#include <asset_parser/parser.hpp>
#include <filesystem>
#include <logger/logger.hpp> #include <logger/logger.hpp>
void try_packing_texture(
const std::filesystem::path &in_path,
const std::filesystem::path &out_path
)
{
auto texture_loader = lt::TextureLoaderFactory::create(in_path.extension().string());
if (!texture_loader)
{
// Don't log anything; this is expected.
return;
}
try
{
Assets::TextureAsset::pack(texture_loader->load(in_path), out_path);
log_inf("Packed a texture asset:");
log_inf("\tloader : {}", texture_loader->get_name());
log_inf("\tin path: {}", in_path.string());
log_inf("\tout path: {}", out_path.string());
}
catch (const std::exception &exp)
{
log_err("Failed to pack texture asset:");
log_err("\tloader : {}", texture_loader->get_name());
log_err("\tin path : {}", in_path.string());
log_err("\tout path: {}", out_path.string());
log_err("\texp.what: {}", exp.what());
}
}
void try_packing_text(const std::filesystem::path &in_path, const std::filesystem::path &out_path)
{
auto text_loader = lt::TextLoaderFactory::create(in_path.extension().string());
if (!text_loader)
{
// Don't log anything; this is expected.
return;
}
try
{
Assets::TextAsset::pack(text_loader->load(in_path), out_path);
log_inf("Packed a text asset:");
log_inf("\tloader : {}", text_loader->get_name());
log_inf("\tin path: {}", in_path.string());
log_inf("\tout path: {}", out_path.string());
}
catch (const std::exception &exp)
{
log_err("Failed to pack a text asset:");
log_err("\tloader : {}", text_loader->get_name());
log_err("\tin path : {}", in_path.string());
log_err("\tout path: {}", out_path.string());
log_err("\texp.what: {}", exp.what());
}
}
auto main(int argc, char *argv[]) -> int32_t auto main(int argc, char *argv[]) -> int32_t
try try
{ {
@ -19,24 +81,20 @@ try
} }
const auto &in_path = directory_iterator.path(); const auto &in_path = directory_iterator.path();
const auto out_path = std::format("{}.asset", in_path.c_str());
if (in_path.extension() == ".vert") auto out_path = in_path;
{ out_path.replace_extension(".asset");
bake_shader(in_path, out_path, lt::assets::ShaderAsset::Type::vertex);
} try_packing_texture(in_path, out_path);
else if (in_path.extension() == ".frag") try_packing_text(in_path, out_path);
{
bake_shader(in_path, out_path, lt::assets::ShaderAsset::Type::fragment);
}
} }
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }
catch (const std::exception &exp) catch (const std::exception &exp)
{ {
lt::log::critical("Terminating due to uncaught exception:"); log_crt("Terminating due to uncaught exception:");
lt::log::critical("\texception.what: {}:", exp.what()); log_crt("\texception.what: {}:", exp.what());
return EXIT_FAILURE; return EXIT_FAILURE;
} }

View file

@ -1,65 +1,184 @@
#pragma once #pragma once
#include <assets/shader.hpp> #include <asset_parser/assets/text.hpp>
#include <asset_parser/assets/texture.hpp>
#include <filesystem>
#include <logger/logger.hpp> #include <logger/logger.hpp>
#include <string_view>
#include <unordered_set>
inline void bake_shader( #define STB_IMAGE_IMPLEMENTATION
const std::filesystem::path &in_path, #include <stb_image.h>
const std::filesystem::path &out_path,
lt::assets::ShaderAsset::Type type namespace lt {
)
class Loader
{ {
using lt::assets::ShaderAsset; public:
using enum lt::assets::ShaderAsset::Type; [[nodiscard]] virtual auto get_name() const -> std::string_view = 0;
auto glsl_path = in_path.string(); Loader() = default;
auto spv_path = std::format("{}.spv", glsl_path);
lt::log::trace(
"Compiling {} shader {} -> {}",
type == vertex ? "vertex" : "fragment",
glsl_path,
spv_path
);
// Don't bother linking to shaderc, just invoke the command with a system call. Loader(Loader &&) = default;
// NOLINTNEXTLINE(concurrency-mt-unsafe)
system(
std::format(
"glslc --target-env=vulkan1.4 -std=450core -fshader-stage={} {} -o {}",
type == vertex ? "vert" : "frag",
glsl_path,
spv_path
)
.c_str()
);
auto stream = std::ifstream(spv_path, std::ios::binary); Loader(const Loader &) = delete;
lt::ensure(
stream.is_open(),
"Failed to open compiled {} shader at: {}",
type == vertex ? "vert" : "frag",
spv_path
);
stream.seekg(0, std::ios::end); auto operator=(Loader &&) -> Loader & = default;
const auto size = stream.tellg();
auto bytes = std::vector<std::byte>(size); auto operator=(const Loader &) -> Loader & = delete;
stream.seekg(0, std::ios::beg);
stream.read((char *)bytes.data(), size); // NOLINT
lt::log::debug("BYTES: {}", bytes.size());
stream.close();
std::filesystem::remove(spv_path);
ShaderAsset::pack( virtual ~Loader() = default;
out_path,
lt::assets::AssetMetadata { private:
.version = lt::assets::current_version, };
.type = ShaderAsset::asset_type_identifier,
}, class TextureLoader: public Loader
ShaderAsset::Metadata { {
.type = type, public:
}, TextureLoader() = default;
std::move(bytes)
); [[nodiscard]] virtual auto load(std::filesystem::path file_path) const
-> Assets::TextureAsset::PackageData
= 0;
};
class StbLoader: public TextureLoader
{
public:
StbLoader() = default;
void load(std::filesystem::path path);
[[nodiscard]] static auto get_supported_extensions() -> std::unordered_set<std::string_view>
{
return { ".png" };
} }
[[nodiscard]] auto get_name() const -> std::string_view override
{
return "StbLoader";
}
[[nodiscard]] auto load(std::filesystem::path file_path) const
-> Assets::TextureAsset::PackageData override
{
auto width = int {};
auto height = int {};
auto channels = int {};
auto *pixels = stbi_load(file_path.string().c_str(), &width, &height, &channels, 4);
if (!pixels)
{
throw std::runtime_error {
std::format("Failed to load image file at: {} using stbi_load", file_path.string()),
};
}
const auto metadata = Assets::Asset::Metadata {
.type = Assets::Asset::Type::Texture,
};
const auto texture_metadata = Assets::TextureAsset::Metadata {
.format = Assets::TextureAsset::Format::RGBA8,
.num_components = static_cast<uint32_t>(channels),
.pixel_size = {
static_cast<uint32_t>(width),
static_cast<uint32_t>(height),
{},
},
};
auto pixels_blob = Assets::Blob {};
pixels_blob.resize(static_cast<size_t>(width) * height * channels);
// TODO(Light): figure out if it's possible to directly populate a blob with stbi functions
memcpy(pixels_blob.data(), pixels, pixels_blob.size());
stbi_image_free(pixels);
return Assets::TextureAsset::PackageData {
.metadata = metadata,
.texture_metadata = texture_metadata,
.pixels = std::move(pixels_blob),
};
}
};
class TextureLoaderFactory
{
public:
static auto create(std::string_view file_extension) -> std::unique_ptr<TextureLoader>
{
if (StbLoader::get_supported_extensions().contains(file_extension))
{
return std::make_unique<StbLoader>();
}
return {};
}
};
class TextLoader: Loader
{
public:
[[nodiscard]] static auto get_supported_extensions() -> std::unordered_set<std::string_view>
{
return { ".glsl", ".txt", ".hlsl" };
}
[[nodiscard]] auto get_name() const -> std::string_view override
{
return "TextLoader";
}
[[nodiscard]] auto load(const std::filesystem::path &file_path) const
-> Assets::TextAsset::PackageData
{
auto stream = std::ifstream { file_path, std::ios::binary };
if (!stream.good())
{
throw std::runtime_error {
std::format(
"Failed to open ifstream for text loading of file: {}",
file_path.string()
),
};
}
auto file_size = std::filesystem::file_size(file_path);
auto text_blob = Assets::Blob(file_size);
stream.read((char *)(text_blob.data()), static_cast<long>(file_size)); // NOLINT
const auto metadata = Assets::Asset::Metadata {
.type = Assets::Asset::Type::Text,
};
const auto text_metadata = Assets::TextAsset::Metadata {
.lines = {},
};
return Assets::TextAsset::PackageData {
.metadata = metadata,
.text_metadata = {},
.text_blob = std::move(text_blob),
};
}
};
class TextLoaderFactory
{
public:
static auto create(std::string_view file_extension) -> std::unique_ptr<TextLoader>
{
if (TextLoader::get_supported_extensions().contains(file_extension))
{
return std::make_unique<TextLoader>();
}
return {};
}
};
} // namespace lt

View file

@ -0,0 +1,9 @@
add_library_module(asset_manager
asset_manager.cpp
)
target_link_libraries(
asset_manager
PUBLIC asset_parser
PRIVATE logger
)

View file

@ -0,0 +1,92 @@
#include <asset_manager/asset_manager.hpp>
#include <asset_parser/assets/text.hpp>
#include <asset_parser/assets/texture.hpp>
#include <logger/logger.hpp>
#include <renderer/graphics_context.hpp>
#include <renderer/shader.hpp>
#include <renderer/texture.hpp>
namespace lt {
/* static */ auto AssetManager::instance() -> AssetManager &
{
static auto instance = AssetManager {};
return instance;
}
void AssetManager::load_shader_impl(
const std::string &name,
const std::filesystem::path &vertex_path,
const std::filesystem::path &pixel_path
)
{
try
{
log_trc("Loading shader:");
log_trc("\tname : {}", name);
log_trc("\tvertex path: {}", vertex_path.string());
log_trc("\tpixel path : {}", pixel_path.string());
m_shaders[name] = Ref<Shader>(Shader::create(
get_or_load_text_asset(vertex_path.string()),
get_or_load_text_asset(pixel_path),
GraphicsContext::get_shared_context()
));
}
catch (const std::exception &exp)
{
log_err("Failed to load shader:");
log_err("\tname : {}", name);
log_err("\tvertex path: {}", vertex_path.string());
log_err("\tpixel path : {}", pixel_path.string());
log_err("\texception : {}", exp.what());
}
}
void AssetManager::load_texture_impl(const std::string &name, const std::filesystem::path &path)
{
try
{
log_trc("Loading texture:");
log_trc("\tname: {}", name);
log_trc("\tpath: {}", path.string());
m_textures[name] = Ref<Texture>(
Texture::create(get_or_load_texture_asset(path), GraphicsContext::get_shared_context())
);
}
catch (const std::exception &exp)
{
log_err("Failed to load texture:");
log_err("\tname : {}", name);
log_err("\tpath : {}", path.string());
log_err("\texception: {}", exp.what());
}
}
auto AssetManager::get_or_load_text_asset(const std::filesystem::path &path)
-> Ref<Assets::TextAsset>
{
const auto key = std::filesystem::canonical(path).string();
if (!m_text_assets.contains(key))
{
m_text_assets.emplace(key, create_ref<Assets::TextAsset>(path));
}
return m_text_assets[key];
}
auto AssetManager::get_or_load_texture_asset(const std::filesystem::path &path)
-> Ref<Assets::TextureAsset>
{
const auto key = std::filesystem::canonical(path).string();
if (!m_texture_assets.contains(key))
{
m_texture_assets.emplace(key, create_ref<Assets::TextureAsset>(path));
}
return m_texture_assets[key];
}
} // namespace lt

View file

@ -0,0 +1,78 @@
#pragma once
#include <filesystem>
namespace Assets {
class TextAsset;
class TextureAsset;
} // namespace Assets
namespace lt {
class Shader;
class Texture;
/**
* Asset is the data on the disk.
* Resource is the data on the gpu/cpu
*
* eg. TextureAsset is the file on the disk
* eg. Texture is the representation of it in the GPU
*/
class AssetManager
{
public:
static void load_shader(
const std::string &name,
const std::filesystem::path &vertex_path,
const std::filesystem::path &pixel_path
)
{
instance().load_shader_impl(name, vertex_path, pixel_path);
}
static void load_texture(const std::string &name, const std::filesystem::path &path)
{
instance().load_texture_impl(name, path);
}
static auto get_shader(const std::string &name) -> Ref<Shader>
{
return instance().m_shaders[name];
}
static auto get_texture(const std::string &name) -> Ref<Texture>
{
return instance().m_textures[name];
}
private:
AssetManager() = default;
static auto instance() -> AssetManager &;
void load_shader_impl(
const std::string &name,
const std::filesystem::path &vertex_path,
const std::filesystem::path &pixel_path
);
void load_texture_impl(const std::string &name, const std::filesystem::path &path);
auto get_or_load_text_asset(const std::filesystem::path &path) -> Ref<Assets::TextAsset>;
auto get_or_load_texture_asset(const std::filesystem::path &path) -> Ref<Assets::TextureAsset>;
std::unordered_map<std::string, Ref<Assets::TextAsset>> m_text_assets;
std::unordered_map<std::string, Ref<Assets::TextureAsset>> m_texture_assets;
std::unordered_map<std::string, Ref<Shader>> m_shaders;
std::unordered_map<std::string, Ref<Texture>> m_textures;
};
} // namespace lt

View file

@ -0,0 +1,11 @@
add_library_module(asset_parser
parser.cpp
assets/texture.cpp
assets/text.cpp
)
target_link_libraries(
asset_parser
PRIVATE LZ4::lz4_static
PRIVATE logger
)

View file

@ -1,4 +1,5 @@
#include <asset_parser/assets/text.hpp> #include <asset_parser/assets/text.hpp>
#include <lz4.h>
namespace Assets { namespace Assets {

View file

@ -0,0 +1,165 @@
#include <asset_parser/assets/texture.hpp>
#include <lz4.h>
namespace Assets {
/* static */ void TextureAsset::pack(const PackageData &data, const std::filesystem::path &out_path)
{
const auto &[metadata, texture_metadata, pixels] = data;
auto stream = std::ofstream { out_path, std::ios::binary | std::ios::trunc };
if (!stream.is_open())
{
throw std::runtime_error {
std::format("Failed to open ofstream for packing texture at: {}", out_path.string())
};
}
stream.seekp(0);
// NOLINTBEGIN(cppcoreguidelines-pro-type-cstyle-cast)
stream.write((char *)&current_version, sizeof(current_version));
stream.write((char *)&metadata, sizeof(metadata));
stream.write((char *)&texture_metadata, sizeof(texture_metadata));
constexpr auto number_of_blobs = uint32_t { 1 };
stream.write((char *)&number_of_blobs, sizeof(number_of_blobs));
auto pixels_metadata = BlobMetadata {
.tag = BlobMetadata::Tag::color,
.offset = static_cast<size_t>(stream.tellp()) + sizeof(BlobMetadata),
.compression_type = CompressionType::None,
.compressed_size = pixels.size(),
.uncompressed_size = pixels.size(),
};
stream.write((char *)&pixels_metadata, sizeof(pixels_metadata));
stream.write((char *)&pixels[0], static_cast<long>(pixels.size()));
// NOLINTEND(cppcoreguidelines-pro-type-cstyle-cast)
}
TextureAsset::TextureAsset(const std::filesystem::path &path)
{
m_stream = std::ifstream { path, std::ios::binary };
if (!m_stream.is_open())
{
throw std::runtime_error {
std::format("Failed to open ifstream for loading texture asset at: {}", path.string())
};
}
// NOLINTBEGIN(cppcoreguidelines-pro-type-cstyle-cast)
m_stream.read((char *)&version, sizeof(version));
m_stream.read((char *)&m_asset_metadata, sizeof(m_asset_metadata));
m_stream.read((char *)&m_metadata, sizeof(m_metadata));
auto num_blobs = uint32_t {};
m_stream.read((char *)&num_blobs, sizeof(num_blobs));
if (num_blobs != 1)
{
throw std::runtime_error {
std::format("Failed to load texture asset: invalid number of blobs: {}", num_blobs)
};
}
m_stream.read((char *)&m_pixel_blob_metadata, sizeof(m_pixel_blob_metadata));
if (m_pixel_blob_metadata.tag != BlobMetadata::Tag::color)
{
throw std::runtime_error {
std::format(
"Failed to load texture asset: invalid blob tag, expected {}, got {}",
std::to_underlying(BlobMetadata::Tag::color),
std::to_underlying(m_pixel_blob_metadata.tag)
),
};
}
// NOLINTEND(cppcoreguidelines-pro-type-cstyle-cast)
}
void TextureAsset::unpack_blob(
BlobMetadata::Tag tag,
std::byte *destination,
size_t destination_capacity
)
{
if (tag != BlobMetadata::Tag::color)
{
throw std::runtime_error {
std::format("Invalid tag for unpack_blob of TextureAsset: {}", std::to_underlying(tag))
};
}
m_stream.seekg(static_cast<long>(m_pixel_blob_metadata.offset));
switch (m_pixel_blob_metadata.compression_type)
{
case Assets::CompressionType::None:
if (m_pixel_blob_metadata.uncompressed_size != m_pixel_blob_metadata.compressed_size)
{
throw std::runtime_error(
"Failed to unpack blob from TextureAsset: "
"compressed/uncompressed size mismatch for no compression "
"type"
);
}
if (m_pixel_blob_metadata.uncompressed_size > destination_capacity)
{
throw std::runtime_error(
"Failed to unpack blob from TextureAsset: "
"uncompressed_size > destination_capacity, unpacking "
"would result in segfault"
);
}
if (!m_stream.is_open())
{
throw std::runtime_error(
"Failed to unpack blob from TextureAsset: ifstream is "
"closed"
);
}
m_stream.read(
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast)
(char *)destination,
static_cast<long>(m_pixel_blob_metadata.uncompressed_size)
);
return;
default:
throw std::runtime_error(
std::format(
"Failed to unpack blob from TextureAsset: unsupported "
"compression type: {}",
std::to_underlying(m_pixel_blob_metadata.compression_type)
)
);
}
}
[[nodiscard]] auto TextureAsset::get_asset_metadata() const -> const Asset::Metadata &
{
return m_asset_metadata;
}
[[nodiscard]] auto TextureAsset::get_metadata() const -> const Metadata &
{
return m_metadata;
}
[[nodiscard]] auto TextureAsset::get_blob_metadata(BlobMetadata::Tag tag) const
-> const BlobMetadata &
{
if (tag != BlobMetadata::Tag::color)
{
throw std::runtime_error { std::format(
"Invalid tag for get_blob_metadata of TextureAsset: {}",
std::to_underlying(tag)
) };
}
return m_pixel_blob_metadata;
}
} // namespace Assets

View file

@ -0,0 +1,62 @@
#include <asset_parser/parser.hpp>
#include <format>
#include <fstream>
#include <utility>
namespace Assets {
// void Asset::unpack(std::byte *destination)
// {
// if (!m_stream.is_open())
// {
// throw std::logic_error {
// "Failed to unpack asset: "
// "ifstream is closed",
// };
// }
//
// switch (m_metadata.blob_compression_type)
// {
// case CompressionType::None:
// if (m_metadata.packed_size != m_metadata.unpacked_size)
// {
// throw std::logic_error {
// "Failed to unpack asset: "
// "compression type set to none but packed/unpacked sizes differ",
// };
// }
//
// m_stream.read(
// std::bit_cast<char *>(destination),
// static_cast<long>(m_metadata.packed_size)
// );
// m_stream.close();
//
// case CompressionType::LZ4:
// m_stream.close();
// throw std::logic_error {
// "Failed to unpack asset: "
// "LZ4 compression is not implemented yet",
// };
//
//
// case CompressionType::LZ4HC:
// m_stream.close();
// throw std::logic_error {
// "Failed to unpack asset: "
// "LZ4HC compression is not implemented yet",
// };
//
// default:
// m_stream.close();
// throw std::logic_error {
// std::format(
// "Failed to unpack asset: "
// "Compression type was not recognized: {}",
// std::to_underlying(m_metadata.blob_compression_type)
// ),
// };
// }
// }
} // namespace Assets

View file

@ -0,0 +1,58 @@
#pragma once
#include <asset_parser/compressors/compressors.hpp>
#include <asset_parser/parser.hpp>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <logger/logger.hpp>
namespace Assets {
class TextAsset: public Asset
{
public:
struct Metadata
{
uint32_t lines {};
};
/** Data required to pack a text asset */
struct PackageData
{
Asset::Metadata metadata;
Metadata text_metadata;
Blob text_blob;
};
static void pack(const PackageData &data, const std::filesystem::path &out_path);
TextAsset(const std::filesystem::path &path);
void unpack_blob(
BlobMetadata::Tag tag,
std::byte *destination,
size_t destination_capacity
) const;
[[nodiscard]] auto get_asset_metadata() const -> const Asset::Metadata &;
[[nodiscard]] auto get_metadata() const -> const Metadata &;
[[nodiscard]] auto get_blob_metadata(BlobMetadata::Tag tag) const -> const BlobMetadata &;
private:
uint32_t version {};
Asset::Metadata m_asset_metadata {};
Metadata m_metadata {};
BlobMetadata m_text_blob_metadata {};
mutable std::ifstream m_stream;
};
} // namespace Assets

View file

@ -0,0 +1,64 @@
#pragma once
#include <asset_parser/compressors/compressors.hpp>
#include <asset_parser/parser.hpp>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <logger/logger.hpp>
namespace Assets {
class TextureAsset: public Asset
{
public:
enum class Format : uint32_t // NOLINT(performance-enum-size)
{
None = 0,
RGBA8,
};
struct Metadata
{
Format format;
uint32_t num_components;
std::array<uint32_t, 3> pixel_size;
};
/** Data required to pack a texture asset */
struct PackageData
{
Asset::Metadata metadata;
Metadata texture_metadata;
Blob pixels;
};
static void pack(const PackageData &data, const std::filesystem::path &out_path);
TextureAsset(const std::filesystem::path &path);
void unpack_blob(BlobMetadata::Tag tag, std::byte *destination, size_t destination_capacity);
[[nodiscard]] auto get_asset_metadata() const -> const Asset::Metadata &;
[[nodiscard]] auto get_metadata() const -> const Metadata &;
[[nodiscard]] auto get_blob_metadata(BlobMetadata::Tag tag) const -> const BlobMetadata &;
private:
uint32_t version {};
Asset::Metadata m_asset_metadata {};
Metadata m_metadata {};
BlobMetadata m_pixel_blob_metadata {};
std::ifstream m_stream;
};
} // namespace Assets

View file

@ -0,0 +1,14 @@
#pragma once
#include <cstdint>
namespace Assets {
enum class CompressionType : uint32_t // NOLINT(performance-enum-size)
{
None,
LZ4,
LZ4HC,
};
}

View file

@ -0,0 +1,68 @@
#pragma once
#include <asset_parser/compressors/compressors.hpp>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <logger/logger.hpp>
#include <utility>
#include <vector>
namespace Assets {
constexpr auto current_version = uint32_t { 1 };
struct BlobMetadata
{
enum class Tag : uint8_t
{
text,
color,
depth,
vertices,
indices,
};
Tag tag;
size_t offset;
CompressionType compression_type;
size_t compressed_size;
size_t uncompressed_size;
};
using Blob = std::vector<std::byte>;
class Asset
{
public:
enum class Type : uint32_t // NOLINT(performance-enum-size)
{
None,
Texture,
Text,
Mesh,
Material,
};
struct Metadata
{
Type type;
};
Asset() = default;
/** Directly unpacks from disk to the destination.
*
* @note The destination MUST have at least blob_metadata.unpacked_size bytes available for
* writing, otherwise segfault could occur!
*/
void unpack_blob(BlobMetadata::Tag blob_tag, std::byte *destination);
};
} // namespace Assets

View file

@ -1,5 +0,0 @@
add_library_module(assets shader.cpp)
target_link_libraries(assets PUBLIC logger lt_debug)
add_test_module(assets shader.test.cpp)

View file

@ -1,148 +0,0 @@
#include <assets/shader.hpp>
namespace lt::assets {
constexpr auto total_metadata_size = //
sizeof(AssetMetadata::type) //
+ sizeof(AssetMetadata::version) //
+ sizeof(ShaderAsset::Metadata::type) //
+ sizeof(BlobMetadata::tag) //
+ sizeof(BlobMetadata::offset) //
+ sizeof(BlobMetadata::compression_type) //
+ sizeof(BlobMetadata::compressed_size) //
+ sizeof(BlobMetadata::uncompressed_size);
ShaderAsset::ShaderAsset(const std::filesystem::path &path): m_stream(path)
{
ensure(m_stream.is_open(), "Failed to open shader asset at: {}", path.string());
const auto read = [this](auto &field) {
m_stream.read(std::bit_cast<char *>(&field), sizeof(field));
};
m_stream.seekg(0, std::ifstream::end);
const auto file_size = static_cast<size_t>(m_stream.tellg());
ensure(
file_size > total_metadata_size,
"Failed to open shader asset at: {}, file smaller than metadata: {} < {}",
path.string(),
total_metadata_size,
file_size
);
m_stream.seekg(0, std::ifstream::beg);
read(m_asset_metadata.type);
read(m_asset_metadata.version);
read(m_metadata.type);
read(m_code_blob_metadata.tag);
read(m_code_blob_metadata.offset);
read(m_code_blob_metadata.compression_type);
read(m_code_blob_metadata.compressed_size);
read(m_code_blob_metadata.uncompressed_size);
ensure(
m_asset_metadata.type == asset_type_identifier,
"Failed to open shader asset at: {}, incorrect asset type: {} != {}",
path.string(),
m_asset_metadata.type,
asset_type_identifier
);
ensure(
m_asset_metadata.version == current_version,
"Failed to open shader asset at: {}, version mismatch: {} != {}",
path.string(),
m_asset_metadata.version,
current_version
);
ensure(
std::to_underlying(m_metadata.type) <= std::to_underlying(Type::compute),
"Failed to open shader asset at: {}, invalid shader type: {}",
path.string(),
std::to_underlying(m_metadata.type)
);
ensure(
m_code_blob_metadata.tag == std::to_underlying(BlobTag::code),
"Failed to open shader asset at: {}, invalid blob tag: {}",
path.string(),
m_code_blob_metadata.tag
);
ensure(
m_code_blob_metadata.offset + m_code_blob_metadata.compressed_size <= file_size,
"Failed to open shader asset at: {}, file smaller than blob: {} > {} + {}",
path.string(),
file_size,
m_code_blob_metadata.offset,
m_code_blob_metadata.compressed_size
);
}
/* static */ void ShaderAsset::pack(
const std::filesystem::path &destination,
AssetMetadata asset_metadata,
Metadata metadata,
Blob code_blob
)
{
auto stream = std::ofstream {
destination,
std::ios::binary | std::ios::trunc,
};
const auto code_blob_metadata = BlobMetadata {
.tag = std::to_underlying(BlobTag::code),
.offset = total_metadata_size,
.compression_type = CompressionType::none,
.compressed_size = code_blob.size(),
.uncompressed_size = code_blob.size(),
};
ensure(stream.is_open(), "Failed to pack shader asset to {}", destination.string());
const auto write = [&stream](auto &field) {
stream.write(std::bit_cast<char *>(&field), sizeof(field));
};
write(asset_metadata.type);
write(asset_metadata.version);
write(metadata.type);
write(code_blob_metadata.tag);
write(code_blob_metadata.offset);
write(code_blob_metadata.compression_type);
write(code_blob_metadata.compressed_size);
write(code_blob_metadata.uncompressed_size);
stream.write(std::bit_cast<char *>(code_blob.data()), static_cast<long long>(code_blob.size()));
}
void ShaderAsset::unpack_to(BlobTag tag, std::span<std::byte> destination) const
{
ensure(tag == BlobTag::code, "Invalid blob tag for shader asset: {}", std::to_underlying(tag));
ensure(
destination.size() >= m_code_blob_metadata.uncompressed_size,
"Failed to unpack shader blob {} to destination ({}) of size {} since it's smaller "
"than the blobl's uncompressed size: {}",
std::to_underlying(tag),
std::bit_cast<size_t>(destination.data()),
destination.size(),
m_code_blob_metadata.uncompressed_size
);
m_stream.seekg(static_cast<long long>(m_code_blob_metadata.offset));
m_stream.read(
std::bit_cast<char *>(destination.data()),
static_cast<long long>(m_code_blob_metadata.uncompressed_size)
);
}
[[nodiscard]] auto ShaderAsset::unpack(BlobTag tag) const -> Blob
{
ensure(tag == BlobTag::code, "Invalid blob tag for shader asset: {}", std::to_underlying(tag));
auto blob = Blob(m_code_blob_metadata.uncompressed_size);
unpack_to(tag, blob);
return blob;
}
} // namespace lt::assets

View file

@ -1,94 +0,0 @@
#include <assets/shader.hpp>
#include <ranges>
#include <test/test.hpp>
using ::lt::assets::AssetMetadata;
using ::lt::assets::BlobMetadata;
using ::lt::assets::ShaderAsset;
using ::lt::test::Case;
using ::lt::test::expect_eq;
using ::lt::test::expect_throw;
using ::lt::test::expect_true;
using ::lt::test::Suite;
const auto test_data_path = std::filesystem::path { "./data/test_assets" };
const auto tmp_path = std::filesystem::path { "/tmp/lt_assets_tests/" };
Suite raii = "shader_raii"_suite = [] {
std::filesystem::current_path(test_data_path);
std::filesystem::create_directories(tmp_path);
Case { "happy path won't throw" } = [] {
};
Case { "many won't freeze/throw" } = [] {
};
Case { "unhappy path throws" } = [] {
expect_throw([] { ShaderAsset { "random_path" }; });
};
};
// NOLINTNEXTLINE(cppcoreguidelines-interfaces-global-init)
Suite packing = "shader_pack"_suite = [] {
Case { "" } = [] {
const auto out_path = tmp_path / "shader_packing";
auto dummy_blob = lt::assets::Blob {};
for (auto idx : std::views::iota(0, 255))
{
dummy_blob.emplace_back(static_cast<std::byte>(idx));
}
const auto expected_size = //
sizeof(AssetMetadata::type) //
+ sizeof(AssetMetadata::version) //
+ sizeof(ShaderAsset::Metadata::type) //
+ sizeof(BlobMetadata::tag) //
+ sizeof(BlobMetadata::offset) //
+ sizeof(BlobMetadata::compression_type) //
+ sizeof(BlobMetadata::compressed_size) //
+ sizeof(BlobMetadata::uncompressed_size) //
+ dummy_blob.size();
ShaderAsset::pack(
out_path,
lt::assets::AssetMetadata {
.version = lt::assets::current_version,
.type = ShaderAsset::asset_type_identifier,
},
ShaderAsset::Metadata {
.type = ShaderAsset::Type::vertex,
},
std::move(dummy_blob)
);
auto stream = std::ifstream {
out_path,
std::ios::binary,
};
expect_true(stream.is_open());
stream.seekg(0, std::ios::end);
const auto file_size = static_cast<size_t>(stream.tellg());
expect_eq(file_size, expected_size);
stream.close();
auto shader_asset = ShaderAsset { out_path };
const auto &asset_metadata = shader_asset.get_asset_metadata();
expect_eq(asset_metadata.type, ShaderAsset::asset_type_identifier);
expect_eq(asset_metadata.version, lt::assets::current_version);
const auto &metadata = shader_asset.get_metadata();
expect_eq(metadata.type, ShaderAsset::Type::vertex);
auto blob = shader_asset.unpack(ShaderAsset::BlobTag::code);
expect_eq(blob.size(), 255);
for (auto idx : std::views::iota(0, 255))
{
expect_eq(blob[idx], static_cast<std::byte>(idx));
}
};
};

View file

@ -1,3 +0,0 @@
#pragma once
// TO BE DOOO

View file

@ -1,42 +0,0 @@
#pragma once
namespace lt::assets {
using Type_T = std::array<const char, 16>;
using Tag_T = uint8_t;
using Version = uint8_t;
using Blob = std::vector<std::byte>;
constexpr auto current_version = Version { 1u };
enum class CompressionType : uint8_t
{
none,
lz4,
lz4_hc,
};
struct AssetMetadata
{
Version version;
Type_T type;
};
struct BlobMetadata
{
Tag_T tag;
size_t offset;
CompressionType compression_type;
size_t compressed_size;
size_t uncompressed_size;
};
} // namespace lt::assets

View file

@ -1,74 +0,0 @@
#pragma once
#include <assets/metadata.hpp>
namespace lt::assets {
class ShaderAsset
{
public:
static constexpr auto asset_type_identifier = Type_T { "SHADER_________" };
enum class BlobTag : Tag_T
{
code,
};
enum class Type : uint8_t
{
vertex,
fragment,
geometry,
compute,
};
struct Metadata
{
Type type;
};
static void pack(
const std::filesystem::path &destination,
AssetMetadata asset_metadata,
Metadata metadata,
Blob code_blob
);
ShaderAsset(const std::filesystem::path &path);
void unpack_to(BlobTag tag, std::span<std::byte> destination) const;
[[nodiscard]] auto unpack(BlobTag tag) const -> Blob;
[[nodiscard]] auto get_asset_metadata() const -> const AssetMetadata &
{
return m_asset_metadata;
}
[[nodiscard]] auto get_metadata() const -> const Metadata &
{
return m_metadata;
}
[[nodiscard]] auto get_blob_metadata(BlobTag tag) const -> const BlobMetadata &
{
ensure(
tag == BlobTag::code,
"Invalid blob tag for shader asset: {}",
std::to_underlying(tag)
);
return m_code_blob_metadata;
}
private:
AssetMetadata m_asset_metadata {};
Metadata m_metadata {};
BlobMetadata m_code_blob_metadata {};
mutable std::ifstream m_stream;
};
} // namespace lt::assets

View file

@ -0,0 +1,2 @@
add_library_module(base)
target_precompile_headers(base INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/private/pch.hpp)

View file

@ -0,0 +1,36 @@
#pragma once
#include <base/base.hpp>
/* windows */
#ifdef _WIN32
#define NOMINMAX
#include <Windows.h>
#undef NOMINMAX
#endif
/** Stdlib */
#include <algorithm>
#include <array>
#include <atomic>
#include <bitset>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iostream>
#include <list>
#include <map>
#include <math.h>
#include <memory>
#include <set>
#include <span>
#include <sstream>
#include <string>
#include <string_view>
#include <thread>
#include <time.h>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>

View file

@ -0,0 +1,91 @@
#pragma once
#include <memory>
namespace lt {
// Ref (Ref)
template<typename t>
using Ref = std::shared_ptr<t>;
template<typename t, typename... Args>
constexpr Ref<t> create_ref(Args &&...args)
{
return std::make_shared<t>(std::forward<Args>(args)...);
}
template<typename t>
constexpr Ref<t> make_ref(t *rawPointer)
{
return std::shared_ptr<t>(rawPointer);
}
// Scope (std::unique_ptr)
template<typename t>
using Scope = std::unique_ptr<t>;
template<typename t, typename... Args>
constexpr std::unique_ptr<t> create_scope(Args &&...args)
{
return std::make_unique<t>(std::forward<Args>(args)...);
}
template<typename t>
constexpr std::unique_ptr<t> make_scope(t *rawPointer)
{
return std::unique_ptr<t>(rawPointer);
}
} // namespace lt
#define lt_win(x) // windows
#define lt_lin(x) // linux
#define lt_mac(x) // mac
enum class Platform : uint8_t
{
windows,
/** Named like so because "linux" is a built-in identifier. */
gnu,
mac,
};
namespace constants {
#if defined(LIGHT_PLATFORM_WINDOWS)
#define lt_win(x)
constexpr auto platform = Platform::windows;
constexpr auto platform_name = "windows";
#undef LIGHT_PLATFORM_WINDOWS
#elif defined(LIGHT_PLATFORM_LINUX)
#define lt_lin(x) x
constexpr auto platform = Platform::gnu;
constexpr auto platform_name = "linux";
#elif defined(LIGHT_PLATFORM_MAC)
#define lt_mac(x) x
constexpr auto platform = Platform::mac;
constexpr auto platform_name = "mac";
#else
#error "Unsupported platform: Unknown"
#endif
} // namespace constants
/* bit-wise */
constexpr auto bit(auto x)
{
return 1 << x;
}
/* token */
#define lt_pair_token_value_to_name(token) { token, #token }
#define lt_pair_token_name_to_value(token) { #token, token }
#define lt_token_name(token) #token

View file

@ -1 +0,0 @@
add_library_module(bitwise)

View file

@ -1,13 +0,0 @@
#pragma once
#include <cstdint>
namespace lt::bitwise {
/* bit-wise */
constexpr auto bit(uint32_t x) -> uint32_t
{
return 1u << x;
}
} // namespace lt::bitwise

View file

@ -1,3 +1,3 @@
add_library_module(camera) add_library_module(camera camera.cpp scene.cpp)
target_link_libraries(camera INTERFACE math) target_link_libraries(camera PUBLIC math)

View file

@ -0,0 +1,6 @@
#include <camera/camera.hpp>
namespace lt {
}

View file

@ -0,0 +1,83 @@
#include <camera/scene.hpp>
#include <math/algebra.hpp>
#include <math/trig.hpp>
namespace lt {
SceneCamera::SceneCamera()
: m_orthographic_specification { .size = 1000.0f, .near_plane = -1.0f, .far_plane = 10000.0f }
, m_perspective_specification { .vertical_fov = math::radians(45.0f),
.near_plane = 0.01f,
.far_plane = 10000.0f }
, m_aspect_ratio(16.0f / 9.0f)
{
calculate_projection();
}
void SceneCamera::set_viewport_size(unsigned int width, unsigned int height)
{
m_aspect_ratio = static_cast<float>(width) / static_cast<float>(height);
calculate_projection();
}
void SceneCamera::set_projection_type(ProjectionType projection_type)
{
m_projection_type = projection_type;
calculate_projection();
}
void SceneCamera::set_orthographic_size(float size)
{
m_orthographic_specification.size = size;
calculate_projection();
}
void SceneCamera::set_orthographic_far_plane(float far_plane)
{
m_orthographic_specification.far_plane = far_plane;
calculate_projection();
}
void SceneCamera::set_orthographic_near_plane(float near_plane)
{
m_orthographic_specification.near_plane = near_plane;
calculate_projection();
}
void SceneCamera::set_perspective_vertical_fov(float vertical_fov)
{
m_perspective_specification.vertical_fov = vertical_fov;
calculate_projection();
}
void SceneCamera::set_perspective_far_plane(float far_plane)
{
m_perspective_specification.far_plane = far_plane;
calculate_projection();
}
void SceneCamera::set_perspective_near_plane(float near_plane)
{
m_perspective_specification.near_plane = near_plane;
calculate_projection();
}
void SceneCamera::calculate_projection()
{
// TODO(Light): implement ortho perspective
if (m_projection_type == ProjectionType::Orthographic)
{
// throw std::runtime_error { "ortho perspective not supported yet" };
}
// defaults to perspective for now...
m_projection = math::perspective(
m_perspective_specification.vertical_fov,
m_aspect_ratio,
m_perspective_specification.near_plane,
m_perspective_specification.far_plane
);
}
} // namespace lt

View file

@ -0,0 +1,35 @@
#pragma once
#include <math/mat4.hpp>
#include <math/vec4.hpp>
namespace lt {
class Camera
{
public:
Camera() = default;
[[nodiscard]] auto get_projection() const -> const math::mat4 &
{
return m_projection;
}
[[nodiscard]] auto get_background_color() const -> const math::vec4 &
{
return m_background_color;
}
void set_background_color(const math::vec4 &color)
{
m_background_color = color;
}
protected:
math::mat4 m_projection;
private:
math::vec4 m_background_color = math::vec4(1.0f, 0.0f, 0.0f, 1.0f);
};
} // namespace lt

View file

@ -0,0 +1,29 @@
#pragma once
#include <camera/scene.hpp>
namespace lt {
struct CameraComponent
{
CameraComponent() = default;
CameraComponent(const CameraComponent &) = default;
CameraComponent(SceneCamera _camera, bool _isPrimary = false)
: camera(_camera)
, isPrimary(_isPrimary)
{
}
operator SceneCamera() const
{
return camera;
}
SceneCamera camera;
bool isPrimary {};
};
} // namespace lt

View file

@ -1,22 +0,0 @@
#pragma once
#include <math/mat4.hpp>
namespace lt::camera::components {
struct PerspectiveCamera
{
float vertical_fov {};
float near_plane {};
float far_plane {};
float aspect_ratio {};
math::vec4 background_color;
bool is_primary {};
};
} // namespace lt::camera::components

View file

@ -0,0 +1,100 @@
#pragma once
#include <camera/camera.hpp>
namespace lt {
class SceneCamera: public Camera
{
public:
enum class ProjectionType
{
Orthographic = 0,
Perspetcive = 1
};
struct OrthographicSpecification
{
float size;
float near_plane;
float far_plane;
};
struct PerspectiveSpecification
{
float vertical_fov;
float near_plane;
float far_plane;
};
SceneCamera();
void set_viewport_size(unsigned int width, unsigned int height);
void set_projection_type(ProjectionType projection_type);
void set_orthographic_size(float size);
void set_orthographic_far_plane(float far_plane);
void set_orthographic_near_plane(float near_plane);
void set_perspective_vertical_fov(float vertical_fov);
void set_perspective_far_plane(float far_plane);
void set_perspective_near_plane(float near_plane);
[[nodiscard]] auto get_orthographic_size() const -> float
{
return m_orthographic_specification.size;
}
[[nodiscard]] auto get_orthographic_far_plane() const -> float
{
return m_orthographic_specification.far_plane;
}
[[nodiscard]] auto get_orthographic_near_plane() const -> float
{
return m_orthographic_specification.near_plane;
}
[[nodiscard]] auto get_perspective_vertical_fov() const -> float
{
return m_perspective_specification.vertical_fov;
}
[[nodiscard]] auto get_perspective_far_plane() const -> float
{
return m_perspective_specification.far_plane;
}
[[nodiscard]] auto get_perspective_near_plane() const -> float
{
return m_perspective_specification.near_plane;
}
[[nodiscard]] auto get_projection_type() const -> ProjectionType
{
return m_projection_type;
}
private:
OrthographicSpecification m_orthographic_specification;
PerspectiveSpecification m_perspective_specification;
float m_aspect_ratio;
ProjectionType m_projection_type { ProjectionType::Orthographic };
void calculate_projection();
};
} // namespace lt

View file

@ -1,4 +1,3 @@
add_library_module(lt_debug instrumentor.cpp) add_library_module(lt_debug instrumentor.cpp)
target_link_libraries(lt_debug PUBLIC logger) target_link_libraries(lt_debug PUBLIC logger)
target_precompile_headers(lt_debug PUBLIC target_precompile_headers(lt_debug PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/private/pch.hpp)
${CMAKE_CURRENT_SOURCE_DIR}/private/pch.hpp)

View file

@ -15,7 +15,7 @@ void Instrumentor::end_session_impl()
{ {
if (m_current_session_count == 0u) if (m_current_session_count == 0u)
{ {
log::warn("0 profiling for the ended session"); log_wrn("0 profiling for the ended session");
} }
m_current_session_count = 0u; m_current_session_count = 0u;

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <format> #include <format>
#include <logger/logger.hpp>
#include <source_location> #include <source_location>
namespace lt { namespace lt {
@ -9,7 +10,7 @@ template<typename Expression_T, typename... Args_T>
struct ensure struct ensure
{ {
ensure( ensure(
const Expression_T &expression, Expression_T expression,
std::format_string<Args_T...> fmt, std::format_string<Args_T...> fmt,
Args_T &&...args, Args_T &&...args,
const std::source_location &location = std::source_location::current() const std::source_location &location = std::source_location::current()
@ -27,6 +28,7 @@ struct ensure
} }
}; };
template<typename Expression_T, typename... Args_T> template<typename Expression_T, typename... Args_T>
ensure(Expression_T, std::format_string<Args_T...>, Args_T &&...) ensure(Expression_T, std::format_string<Args_T...>, Args_T &&...)
-> ensure<Expression_T, Args_T...>; -> ensure<Expression_T, Args_T...>;

View file

@ -1,4 +1,4 @@
add_library_module(ecs sparse_set.cpp) add_library_module(ecs entity.cpp scene.cpp uuid.cpp )
target_link_libraries(ecs PUBLIC logger lt_debug memory) target_link_libraries(ecs
PUBLIC logger lt_debug EnTT::EnTT input camera math
add_test_module(ecs sparse_set.test.cpp registry.test.cpp) )

View file

@ -0,0 +1,10 @@
#include <ecs/entity.hpp>
#include <ecs/scene.hpp>
namespace lt {
Entity::Entity(entt::entity handle, Scene *scene): m_handle(handle), m_scene(scene)
{
}
} // namespace lt

View file

@ -1,351 +0,0 @@
#include <ecs/registry.hpp>
#include <ranges>
#include <test/expects.hpp>
#include <test/test.hpp>
using lt::test::Case;
using lt::test::expect_unreachable;
using lt::test::Suite;
using lt::test::expect_eq;
using lt::test::expect_false;
using lt::test::expect_true;
using lt::ecs::EntityId;
using lt::ecs::Registry;
struct Component
{
int m_int {};
std::string m_string;
[[nodiscard]] friend auto operator==(const Component &lhs, const Component &rhs) -> bool
{
return lhs.m_int == rhs.m_int && lhs.m_string == rhs.m_string;
}
};
template<>
struct std::formatter<Component>
{
constexpr auto parse(std::format_parse_context &context)
{
return context.begin();
}
auto format(const Component &val, std::format_context &context) const
{
return std::format_to(context.out(), "{}, {}", val.m_int, val.m_string);
}
};
struct Component_B
{
float m_float {};
[[nodiscard]] friend auto operator==(const Component_B lhs, const Component_B &rhs) -> bool
{
return lhs.m_float == rhs.m_float;
}
};
template<>
struct std::formatter<Component_B>
{
constexpr auto parse(std::format_parse_context &context)
{
return context.begin();
}
auto format(const Component_B &val, std::format_context &context) const
{
return std::format_to(context.out(), "{}", val.m_float);
}
};
Suite raii = "raii"_suite = [] {
Case { "happy path won't throw" } = [] {
std::ignore = Registry {};
};
Case { "many won't freeze/throw" } = [] {
for (auto idx : std::views::iota(0, 100'000))
{
std::ignore = Registry {};
}
};
Case { "unhappy path throws" } = [] {
};
Case { "post construct has correct state" } = [] {
auto registry = Registry {};
expect_eq(registry.get_entity_count(), 0);
};
};
Suite entity_raii = "entity_raii"_suite = [] {
Case { "create_entity returns unique values" } = [] {
auto registry = Registry {};
auto set = std::unordered_set<EntityId> {};
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = registry.create_entity();
expect_false(set.contains(entity));
set.insert(entity);
expect_eq(set.size(), idx + 1);
}
};
Case { "post create/destroy_entity has correct state" } = [] {
auto registry = Registry {};
auto entities = std::vector<EntityId> {};
for (auto idx : std::views::iota(0, 10'000))
{
entities.emplace_back(registry.create_entity());
expect_eq(registry.get_entity_count(), idx + 1);
}
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = entities.back();
registry.destroy_entity(entity);
entities.pop_back();
expect_eq(registry.get_entity_count(), 10'000 - (idx + 1));
}
};
};
Suite component_raii = "component_raii"_suite = [] {
Case { "add has correct state" } = [] {
auto registry = Registry {};
for (auto idx : std::views::iota(0, 100'000))
{
auto entity = registry.create_entity();
auto &component = registry.add<Component>(
entity,
{ .m_int = idx, .m_string = std::to_string(idx) }
);
expect_eq(component.m_int, idx);
expect_eq(component.m_string, std::to_string(idx));
}
};
Case { "remove has correct state" } = [] {
auto registry = Registry {};
for (auto idx : std::views::iota(0, 100'000))
{
auto entity = registry.create_entity();
auto &component = registry.add<Component>(
entity,
{ .m_int = idx, .m_string = std::to_string(idx) }
);
expect_eq(component.m_int, idx);
expect_eq(component.m_string, std::to_string(idx));
}
};
};
Suite callbacks = "callbacks"_suite = [] {
Case { "connecting on_construct/on_destruct won't throw" } = [] {
auto registry = Registry {};
registry.connect_on_construct<Component>([&](Registry &, EntityId) {});
registry.connect_on_destruct<Component>([&](Registry &, EntityId) {});
};
Case { "on_construct/on_destruct won't get called on unrelated component" } = [] {
auto registry = Registry {};
registry.connect_on_construct<Component>([&](Registry &, EntityId) {
expect_unreachable();
});
registry.connect_on_destruct<Component>([&](Registry &, EntityId) {
expect_unreachable();
});
for (auto idx : std::views::iota(0, 100'000))
{
registry.add<Component_B>(registry.create_entity(), {});
}
};
Case { "on_construct/on_destruct gets called" } = [] {
auto registry = Registry {};
auto all_entities = std::vector<EntityId> {};
auto on_construct_called = std::vector<EntityId> {};
auto on_destruct_called = std::vector<EntityId> {};
registry.connect_on_construct<Component>([&](Registry &, EntityId entity) {
on_construct_called.emplace_back(entity);
});
registry.connect_on_destruct<Component>([&](Registry &, EntityId entity) {
on_destruct_called.emplace_back(entity);
});
expect_true(on_construct_called.empty());
expect_true(on_destruct_called.empty());
for (auto idx : std::views::iota(0, 100'000))
{
auto entity = all_entities.emplace_back(registry.create_entity());
registry.add<Component>(entity, {});
}
expect_eq(on_construct_called, all_entities);
expect_true(on_destruct_called.empty());
for (auto &entity : all_entities)
{
registry.remove<Component>(entity);
}
expect_eq(on_construct_called, all_entities);
expect_eq(on_destruct_called, all_entities);
};
};
Suite each = "each"_suite = [] {
auto registry = Registry {};
auto shared_entity_counter = 0u;
auto component_map_a = std::unordered_map<EntityId, Component> {};
auto entities_a = std::vector<EntityId> {};
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = entities_a.emplace_back(registry.create_entity());
auto &component = registry.add<Component>(
entity,
{ .m_int = idx, .m_string = std::to_string(idx) }
);
component_map_a[entity] = component;
}
auto component_map_b = std::unordered_map<lt::ecs::EntityId, Component_B> {};
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = EntityId {};
if (idx % 3 == 0)
{
entity = entities_a[idx];
++shared_entity_counter;
}
else
{
entity = registry.create_entity();
}
auto &component = registry.add<Component_B>(
entity,
{ .m_float = static_cast<float>(idx) / 2.0f }
);
component_map_b[entity] = component;
}
Case { "each one element" } = [&] {
auto counter = 0u;
registry.each<Component>([&](EntityId entity, Component &component) {
++counter;
expect_eq(component_map_a[entity], component);
});
expect_eq(component_map_a.size(), counter);
counter = 0u;
registry.each<Component_B>([&](EntityId entity, Component_B &component) {
++counter;
expect_eq(component_map_b[entity], component);
});
expect_eq(component_map_b.size(), counter);
};
Case { "each two element" } = [&] {
auto counter = 0u;
registry.each<Component, Component_B>(
[&](EntityId entity, Component &component_a, Component_B &component_b) {
expect_eq(component_map_a[entity], component_a);
expect_eq(component_map_b[entity], component_b);
++counter;
}
);
expect_eq(counter, shared_entity_counter);
};
};
Suite views = "views"_suite = [] {
auto registry = Registry {};
auto shared_entity_counter = 0u;
auto component_map_a = std::unordered_map<EntityId, Component> {};
auto entities_a = std::vector<EntityId> {};
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = entities_a.emplace_back(registry.create_entity());
auto &component = registry.add<Component>(
entity,
{ .m_int = idx, .m_string = std::to_string(idx) }
);
component_map_a[entity] = component;
}
auto component_map_b = std::unordered_map<EntityId, Component_B> {};
for (auto idx : std::views::iota(0, 10'000))
{
auto entity = EntityId {};
if (idx % 3 == 0)
{
entity = entities_a[idx];
++shared_entity_counter;
}
else
{
entity = registry.create_entity();
}
auto &component = registry.add<Component_B>(
entity,
{ .m_float = static_cast<float>(idx) / 2.0f }
);
component_map_b[entity] = component;
}
Case { "view one component" } = [&] {
for (const auto &[entity, component] : registry.view<Component>())
{
expect_eq(component_map_a[entity], component);
}
for (const auto &[entity, component] : registry.view<Component_B>())
{
expect_eq(component_map_b[entity], component);
}
};
Case { "view two component" } = [&] {
auto counter = 0u;
for (const auto &[entity, component, component_b] : registry.view<Component, Component_B>())
{
expect_eq(component_map_a[entity], component);
expect_eq(component_map_b[entity], component_b);
++counter;
}
expect_eq(counter, shared_entity_counter);
counter = 0u;
for (const auto &[entity, component_b, component] : registry.view<Component_B, Component>())
{
expect_eq(component_map_b[entity], component_b);
expect_eq(component_map_a[entity], component);
++counter;
}
expect_eq(counter, shared_entity_counter);
};
};

View file

@ -0,0 +1,31 @@
#include <ecs/components.hpp>
#include <ecs/entity.hpp>
#include <ecs/scene.hpp>
namespace lt {
auto Scene::create_entity(const std::string &name, const TransformComponent &transform) -> Entity
{
return create_entity_with_uuid(name, UUID(), transform);
}
auto Scene::get_entity_by_tag(const std::string &tag) -> Entity
{
return {};
}
auto Scene::create_entity_with_uuid(
const std::string &name,
UUID uuid,
const TransformComponent &transform
) -> Entity
{
auto entity = Entity { m_registry.create(), this };
entity.add_component<TagComponent>(name);
entity.add_component<TransformComponent>(transform);
entity.add_component<UUIDComponent>(uuid);
return entity;
}
} // namespace lt

View file

@ -0,0 +1,325 @@
#include <asset_manager/asset_manager.hpp>
#include <camera/component.hpp>
#include <ecs/components.hpp>
#include <ecs/serializer.hpp>
#include <math/vec3.hpp>
#include <math/vec4.hpp>
#include <yaml-cpp/yaml.h>
namespace YAML {
template<>
struct convert<lt::math::vec3>
{
static auto encode(const lt::math::vec3 &rhs) -> Node
{
auto node = Node {};
node.push_back(rhs.x);
node.push_back(rhs.y);
node.push_back(rhs.z);
return node;
}
static auto decode(const Node &node, lt::math::vec3 &rhs) -> bool
{
if (!node.IsSequence() || node.size() != 3)
{
return false;
}
rhs.x = node[0].as<float>();
rhs.y = node[1].as<float>();
rhs.z = node[2].as<float>();
return true;
}
};
template<>
struct convert<lt::math::vec4>
{
static auto encode(const lt::math::vec4 &rhs) -> Node
{
auto node = Node {};
node.push_back(rhs.x);
node.push_back(rhs.y);
node.push_back(rhs.z);
node.push_back(rhs.w);
return node;
}
static auto decode(const Node &node, lt::math::vec4 &rhs) -> bool
{
if (!node.IsSequence() || node.size() != 4)
{
return false;
}
rhs.x = node[0].as<float>();
rhs.y = node[1].as<float>();
rhs.z = node[2].as<float>();
rhs.w = node[3].as<float>();
return true;
}
};
} // namespace YAML
namespace lt {
auto operator<<(YAML::Emitter &out, const math::vec3 &v) -> YAML::Emitter &
{
out << YAML::Flow;
out << YAML::BeginSeq << v.x << v.y << v.z << YAML::EndSeq;
return out;
}
auto operator<<(YAML::Emitter &out, const math::vec4 &v) -> YAML::Emitter &
{
out << YAML::Flow;
out << YAML::BeginSeq << v.x << v.y << v.z << v.w << YAML::EndSeq;
return out;
}
SceneSerializer::SceneSerializer(const Ref<Scene> &scene): m_scene(scene)
{
}
void SceneSerializer::serialize(const std::string &filePath)
{
auto out = YAML::Emitter {};
out << YAML::BeginMap; // Scene
out << YAML::Key << "Scene" << YAML::Value << "Untitled";
out << YAML::Key << "Entities" << YAML::Value << YAML::BeginSeq;
for (auto [entityID, storage] : m_scene->m_registry.storage())
{
auto entity = Entity { static_cast<entt::entity>(entityID), m_scene.get() };
if (!entity.is_valid())
{
return;
}
serialize_entity(out, entity);
};
out << YAML::EndSeq;
out << YAML::EndMap;
std::filesystem::create_directories(filePath.substr(0ull, filePath.find_last_of('\\')));
auto fout = std::ofstream { filePath };
if (!fout.is_open())
{
log_trc("Failed to create fout at: {}", filePath);
}
fout << out.c_str();
}
auto SceneSerializer::deserialize(const std::string &file_path) -> bool
{
auto stream = std::ifstream { file_path };
auto ss = std::stringstream {};
ss << stream.rdbuf();
auto data = YAML::Load(ss.str());
if (!data["Scene"])
{
return false;
}
auto sceneName = data["Scene"].as<std::string>();
log_trc("Deserializing scene: '{}'", sceneName);
auto entities = data["Entities"];
if (entities)
{
auto texturePaths = std::unordered_set<std::string> {};
for (auto entity : entities)
{
auto uuid = entity["entity"].as<uint64_t>(); // #todo
auto name = std::string {};
auto tagComponent = entity["TagComponent"];
if (tagComponent)
{
name = tagComponent["Tag"].as<std::string>();
}
log_trc("Deserialized entity '{}' : '{}'", uuid, name);
auto deserializedEntity = m_scene->create_entity_with_uuid(name, uuid);
auto gg = deserializedEntity.get_component<TagComponent>();
log_trc("tag: {}", gg.tag);
auto transformComponent = entity["TransformComponent"];
if (transformComponent)
{
auto &entityTransforomComponent = deserializedEntity
.get_component<TransformComponent>();
entityTransforomComponent.translation = transformComponent["Translation"]
.as<math::vec3>();
entityTransforomComponent.rotation = transformComponent["Rotation"]
.as<math::vec3>();
entityTransforomComponent.scale = transformComponent["Scale"].as<math::vec3>();
}
/* #TEMPORARY SOLUTION# */
auto spriteRendererComponent = entity["SpriteRendererComponent"];
if (spriteRendererComponent)
{
auto &entitySpriteRendererComponent = deserializedEntity
.add_component<SpriteRendererComponent>();
entitySpriteRendererComponent.tint = spriteRendererComponent["Tint"]
.as<math::vec4>();
auto texturePath = spriteRendererComponent["Texture"].as<std::string>();
if (!texturePaths.contains(texturePath))
{
AssetManager::load_texture(texturePath, texturePath);
texturePaths.insert(texturePath);
}
entitySpriteRendererComponent.texture = AssetManager::get_texture(texturePath);
}
/* #TEMPORARY SOLUTION# */
auto cameraComponent = entity["CameraComponent"];
if (cameraComponent)
{
auto &entityCameraComponent = deserializedEntity.add_component<CameraComponent>();
const auto &cameraSpecifications = cameraComponent["Camera"];
entityCameraComponent.camera.set_projection_type(
(SceneCamera::ProjectionType)cameraSpecifications["ProjectionType"].as<int>()
);
entityCameraComponent.camera.set_orthographic_size(
cameraSpecifications["OrthographicSize"].as<float>()
);
entityCameraComponent.camera.set_orthographic_near_plane(
cameraSpecifications["OrthographicNearPlane"].as<float>()
);
entityCameraComponent.camera.set_orthographic_far_plane(
cameraSpecifications["OrthographicFarPlane"].as<float>()
);
entityCameraComponent.camera.set_perspective_vertical_fov(
cameraSpecifications["PerspectiveVerticalFOV"].as<float>()
);
entityCameraComponent.camera.set_perspective_near_plane(
cameraSpecifications["PerspectiveNearPlane"].as<float>()
);
entityCameraComponent.camera.set_perspective_far_plane(
cameraSpecifications["PerspectiveFarPlane"].as<float>()
);
entityCameraComponent.camera.set_background_color(
cameraSpecifications["BackgroundColor"].as<math::vec4>()
);
entityCameraComponent.isPrimary = cameraComponent["IsPrimary"].as<bool>();
}
}
return true;
}
return false;
}
void SceneSerializer::serialize_binary(const std::string & /*filePath*/)
{
log_err("NO_IMPLEMENT");
}
auto SceneSerializer::deserialize_binary(const std::string & /*filePath*/) -> bool
{
log_err("NO_IMPLEMENT");
return false;
}
void SceneSerializer::serialize_entity(YAML::Emitter &out, Entity entity)
{
out << YAML::BeginMap; // entity
out << YAML::Key << "entity" << YAML::Value << entity.get_uuid(); // dummy uuid
if (entity.has_component<TagComponent>())
{
out << YAML::Key << "TagComponent";
out << YAML::BeginMap; // tag component
auto &tagComponent = entity.get_component<TagComponent>().tag;
out << YAML::Key << "Tag" << YAML::Value << tagComponent;
out << YAML::EndMap; // tag component
}
if (entity.has_component<TransformComponent>())
{
out << YAML::Key << "TransformComponent";
out << YAML::BeginMap; // transform component
auto &transformComponent = entity.get_component<TransformComponent>();
out << YAML::Key << "Translation" << YAML::Value << transformComponent.translation;
out << YAML::Key << "Rotation" << YAML::Value << transformComponent.rotation;
out << YAML::Key << "Scale" << YAML::Value << transformComponent.scale;
out << YAML::EndMap; // transform component;
}
if (entity.has_component<SpriteRendererComponent>())
{
// TODO(Light): get scene serialization/de-serialization working.
// out << YAML::Key << "SpriteRendererComponent";
// out << YAML::BeginMap; // sprite renderer component;
// auto &spriteRendererComponent = entity.get_component<SpriteRendererComponent>();
// out << YAML::Key << "Texture" << YAML::Value
// << spriteRendererComponent.texture->GetFilePath();
// out << YAML::Key << "Tint" << YAML::Value << spriteRendererComponent.tint;
// out << YAML::EndMap; // sprite renderer component
}
// #todo:
// if(entity.has_component<NativeScriptComponent>())
if (entity.has_component<CameraComponent>())
{
out << YAML::Key << "CameraComponent";
out << YAML::BeginMap; // camera component
auto &cameraComponent = entity.get_component<CameraComponent>();
out << YAML::Key << "Camera" << YAML::Value;
out << YAML::BeginMap; // camera
out << YAML::Key << "OrthographicSize" << YAML::Value
<< cameraComponent.camera.get_orthographic_size();
out << YAML::Key << "OrthographicFarPlane" << YAML::Value
<< cameraComponent.camera.get_orthographic_far_plane();
out << YAML::Key << "OrthographicNearPlane" << YAML::Value
<< cameraComponent.camera.get_orthographic_near_plane();
out << YAML::Key << "PerspectiveVerticalFOV" << YAML::Value
<< cameraComponent.camera.get_perspective_vertical_fov();
out << YAML::Key << "PerspectiveFarPlane" << YAML::Value
<< cameraComponent.camera.get_perspective_far_plane();
out << YAML::Key << "PerspectiveNearPlane" << YAML::Value
<< cameraComponent.camera.get_perspective_near_plane();
out << YAML::Key << "ProjectionType" << YAML::Value
<< (int)cameraComponent.camera.get_projection_type();
out << YAML::Key << "BackgroundColor" << YAML::Value
<< cameraComponent.camera.get_background_color();
out << YAML::EndMap; // camera
out << YAML::Key << "IsPrimary" << YAML::Value << cameraComponent.isPrimary;
out << YAML::EndMap; // camera component
}
out << YAML::EndMap; // entity
}
} // namespace lt

View file

@ -1,163 +0,0 @@
#include <ecs/sparse_set.hpp>
#include <ranges>
#include <test/expects.hpp>
#include <test/test.hpp>
using lt::test::Case;
using lt::test::Suite;
using lt::test::expect_eq;
using lt::test::expect_false;
using lt::test::expect_ne;
using lt::test::expect_throw;
using lt::test::expect_true;
using Set = lt::ecs::SparseSet<int>;
constexpr auto capacity = 100;
Suite raii = "raii"_suite = [] {
Case { "happy path won't throw" } = [] {
std::ignore = Set {};
std::ignore = Set { Set::max_capacity };
};
Case { "unhappy path throws" } = [] {
expect_throw([] { std::ignore = Set { Set::max_capacity + 1 }; });
};
Case { "post construct has correct state" } = [&] {
auto set = Set { capacity };
expect_eq(set.get_size(), 0);
expect_eq(set.get_capacity(), capacity);
};
};
Suite element_raii = "element_raii"_suite = [] {
Case { "many inserts/removes won't freeze/throw" } = [] {
auto set = Set {};
for (auto idx : std::views::iota(0, 10'000))
{
set.insert(idx, {});
}
for (auto idx : std::views::iota(0, 10'000))
{
set.remove(idx);
}
};
Case { "insert returns reference to inserted value" } = [] {
auto set = Set {};
for (auto idx : std::views::iota(0, 10'000))
{
const auto val = Set::Dense_T { idx, {} };
expect_eq(set.insert(val.first, val.second), val);
}
};
Case { "post insert/remove has correct state" } = [] {
auto set = Set {};
for (auto idx : std::views::iota(0, 10'000))
{
set.insert(idx, idx * 2);
expect_eq(set.get_size(), idx + 1);
expect_eq(set.at(idx), Set::Dense_T { idx, idx * 2 });
expect_true(set.contains(idx));
}
for (auto idx : std::views::iota(0, 10'000))
{
expect_eq(set.at(idx), Set::Dense_T { idx, idx * 2 });
expect_true(set.contains(idx));
}
for (auto idx : std::views::iota(0, 10'000))
{
set.remove(idx);
expect_eq(set.get_size(), 10'000 - (idx + 1));
expect_false(set.contains(idx));
expect_throw([&] { std::ignore = set.at(idx); });
}
};
Case { "removed elements won't be iterated again" } = [] {
auto set = Set {};
for (auto idx : std::views::iota(0, 10'000))
{
set.insert(idx, idx);
}
set.remove(0);
set.remove(32);
set.remove(69);
set.remove(420);
set.remove(9'999);
for (auto &[identifier, value] : set)
{
expect_eq(identifier, value);
expect_ne(value, 0);
expect_ne(value, 32);
expect_ne(value, 69);
expect_ne(value, 420);
expect_ne(value, 9'999);
}
};
};
Suite getters = "getters"_suite = [] {
Case { "get_size returns correct values" } = [] {
auto set = Set {};
for (auto idx : std::views::iota(0, 10'000))
{
expect_eq(set.get_size(), idx);
set.insert(idx, {});
}
expect_eq(set.get_size(), 10'000);
};
Case { "get_capacity returns correct values" } = [] {
auto set = Set { 10'000 };
for (auto idx : std::views::iota(0, 10'000))
{
expect_eq(set.get_capacity(), 10'000); // are we testing std::vector's implementation?
set.insert(idx, {});
}
expect_eq(set.get_capacity(), 10'000);
set.insert(set.get_size(), {});
expect_ne(set.get_capacity(), 10'000);
};
Case { "at throws with out of bound access" } = [] {
auto set = Set {};
for (auto idx : std::views::iota(0, 50))
{
expect_throw([&] {
set.insert(idx, {});
std::ignore = set.at(50);
});
}
set.insert(50, {});
std::ignore = set.at(50); // should not throw
};
};
Suite clear = "clear"_suite = [] {
Case { "post clear has correct state" } = [] {
auto set = Set { 0 };
for (auto idx : std::views::iota(0, 10'000))
{
set.insert(idx, {});
}
set.clear();
expect_eq(set.get_size(), 0);
};
};

View file

@ -0,0 +1,14 @@
#include <ecs/uuid.hpp>
namespace lt {
std::mt19937_64 UUID::s_engine = std::mt19937_64(std::random_device()());
std::uniform_int_distribution<uint64_t>
UUID::s_distribution = std::uniform_int_distribution<uint64_t> {};
UUID::UUID(uint64_t uuid /* = -1 */): m_uuid(uuid == -1 ? s_distribution(s_engine) : uuid)
{
}
} // namespace lt

View file

@ -0,0 +1,6 @@
#pragma once
#include <ecs/components/native_script.hpp>
#include <ecs/components/sprite_renderer.hpp>
#include <ecs/components/tag.hpp>
#include <ecs/components/transform.hpp>

View file

@ -0,0 +1,28 @@
#pragma once
#include <ecs/components/scriptable_entity.hpp>
namespace lt {
struct NativeScriptComponent
{
NativeScript *(*CreateInstance)();
void (*DestroyInstance)(NativeScriptComponent *);
template<typename t>
void bind()
{
CreateInstance = []() {
return static_cast<NativeScript *>(new t());
};
DestroyInstance = [](NativeScriptComponent *nsc) {
delete (t *)(nsc->instance);
nsc->instance = nullptr;
};
}
NativeScript *instance;
};
} // namespace lt

View file

@ -0,0 +1,46 @@
#pragma once
#include <ecs/entity.hpp>
namespace lt {
class NativeScript
{
public:
friend class Scene;
NativeScript() = default;
virtual ~NativeScript() = default;
[[nodiscard]] auto get_uid() const -> unsigned int
{
return m_unique_identifier;
}
template<typename t>
auto GetComponent() -> t &
{
return m_entity.get_component<t>();
}
protected:
virtual void on_create()
{
}
virtual void on_destroy()
{
}
virtual void on_update(float ts)
{
}
private:
Entity m_entity;
unsigned int m_unique_identifier = 0; // :#todo
};
} // namespace lt

View file

@ -0,0 +1,35 @@
#pragma once
#include <math/vec4.hpp>
#include <utility>
namespace lt {
class Texture;
struct SpriteRendererComponent
{
SpriteRendererComponent() = default;
SpriteRendererComponent(const SpriteRendererComponent &) = default;
SpriteRendererComponent(
Ref<Texture> _texture,
const math::vec4 &_tint = math::vec4 { 1.0f, 1.0f, 1.0f, 1.0f }
)
: texture(std::move(std::move(_texture)))
, tint(_tint)
{
}
operator Ref<Texture>() const
{
return texture;
}
Ref<Texture> texture;
math::vec4 tint {};
};
} // namespace lt

View file

@ -0,0 +1,30 @@
#pragma once
#include <utility>
namespace lt {
struct TagComponent
{
TagComponent() = default;
TagComponent(const TagComponent &) = default;
TagComponent(std::string _tag): tag(std::move(_tag))
{
}
operator std::string() const
{
return tag;
}
operator const std::string &() const
{
return tag;
}
std::string tag = "Unnamed";
};
} // namespace lt

View file

@ -0,0 +1,43 @@
#pragma once
#include <math/mat4.hpp>
#include <math/vec3.hpp>
namespace lt {
struct TransformComponent
{
TransformComponent(const TransformComponent &) = default;
TransformComponent(
const math::vec3 &_translation = math::vec3(0.0f, 0.0f, 0.0f),
const math::vec3 &_scale = math::vec3(1.0f, 1.0f, 1.0f),
const math::vec3 &_rotation = math::vec3(0.0f, 0.0f, 0.0f)
)
: translation(_translation)
, scale(_scale)
, rotation(_rotation)
{
}
[[nodiscard]] auto get_transform() const -> math::mat4
{
return math::translate(translation)
* math::rotate(rotation.z, math::vec3 { 0.0f, 0.0f, 1.0f }) //
* math::scale(scale);
}
operator const math::mat4() const
{
return get_transform();
}
math::vec3 translation;
math::vec3 scale;
math::vec3 rotation;
};
} // namespace lt

View file

@ -0,0 +1,18 @@
#pragma once
#include <ecs/uuid.hpp>
namespace lt {
struct UUIDComponent
{
UUIDComponent(UUID _uuid): uuid(_uuid)
{
}
UUIDComponent(const UUIDComponent &) = default;
UUID uuid;
};
} // namespace lt

View file

@ -1,54 +1,59 @@
#pragma once #pragma once
#include <ecs/registry.hpp> #include <ecs/components/uuid.hpp>
#include <memory/reference.hpp> #include <ecs/scene.hpp>
#include <entt/entt.hpp>
namespace lt::ecs { namespace lt {
/** High-level entity convenience wrapper */
class Entity class Entity
{ {
public: public:
Entity(memory::Ref<Registry> registry, EntityId identifier) Entity(entt::entity handle = entt::null, Scene *scene = nullptr);
: m_registry(std::move(registry))
, m_identifier(identifier) template<typename t, typename... Args>
auto add_component(Args &&...args) -> t &
{ {
ensure(m_registry, "Failed to create Entity ({}): null registry", m_identifier); return m_scene->m_registry.emplace<t>(m_handle, std::forward<Args>(args)...);
} }
template<typename Component_T> template<typename t>
auto add(Component_T component) -> Component_T & auto get_component() -> t &
{ {
return m_registry->add(m_identifier, component); return m_scene->m_registry.get<t>(m_handle);
} }
template<typename Component_T> template<typename t>
auto get() -> Component_T & auto has_component() -> bool
{ {
return m_registry->get<Component_T>(m_identifier); return m_scene->m_registry.any_of<t>(m_handle);
} }
template<typename Component_T> template<typename t>
auto get() const -> const Component_T & void remove_component()
{ {
return m_registry->get<Component_T>(m_identifier); m_scene->m_registry.remove<t>(m_handle);
} }
auto get_registry() -> memory::Ref<Registry> auto get_uuid() -> uint64_t
{ {
return m_registry; return get_component<UUIDComponent>().uuid;
} }
[[nodiscard]] auto id() const -> EntityId [[nodiscard]] auto is_valid() const -> bool
{ {
return m_identifier; return m_handle != entt::null && m_scene != nullptr;
}
operator uint32_t()
{
return (uint32_t)m_handle;
} }
private: private:
memory::Ref<Registry> m_registry; entt::entity m_handle;
EntityId m_identifier; Scene *m_scene;
}; };
} // namespace lt
} // namespace lt::ecs

View file

@ -1,260 +0,0 @@
#pragma once
#include <ecs/sparse_set.hpp>
#include <memory/scope.hpp>
namespace lt::ecs {
using EntityId = uint32_t;
constexpr auto null_entity = std::numeric_limits<EntityId>::max();
/** A registry of components, the heart of an ECS architecture.
*
* @todo(Light): Implement grouping
* @todo(Light): Implement identifier recycling
* @todo(Light): Optimize views/each
* @todo(Light): Support >2 component views
* @todo(Light): Handle more edge cases or specify the undefined behaviors
*
* @ref https://skypjack.github.io/
* @ref https://github.com/alecthomas/entityx
* @ref https://github.com/skypjack/entt
* @ref https://github.com/SanderMertens/flecs
*/
class Registry
{
public:
using UnderlyingSparseSet_T = TypeErasedSparseSet<EntityId>;
using Callback_T = std::function<void(Registry &, EntityId)>;
template<typename Component_T>
void connect_on_construct(Callback_T callback)
{
m_on_construct_hooks[get_type_id<Component_T>()] = callback;
}
template<typename Component_T>
void connect_on_destruct(Callback_T callback)
{
m_on_destruct_hooks[get_type_id<Component_T>()] = callback;
}
template<typename Component_T>
void disconnect_on_construct()
{
m_on_construct_hooks.erase(get_type_id<Component_T>());
}
template<typename Component_T>
void disconnect_on_destruct()
{
m_on_destruct_hooks.erase(get_type_id<Component_T>());
}
auto create_entity() -> EntityId
{
++m_entity_count;
return m_current++;
}
void destroy_entity(EntityId entity)
{
for (const auto &[key, set] : m_sparsed_sets)
{
set->remove(entity);
}
--m_entity_count;
}
template<typename Component_T>
auto get(EntityId entity) const -> const Component_T &
{
auto &derived_set = get_derived_set<Component_T>();
return derived_set.at(entity).second;
}
template<typename Component_T>
auto get(EntityId entity) -> Component_T &
{
auto &derived_set = get_derived_set<Component_T>();
return derived_set.at(entity).second;
}
template<typename Component_T>
auto add(EntityId entity, Component_T component) -> Component_T &
{
auto &derived_set = get_derived_set<Component_T>();
auto &added_component = derived_set.insert(entity, std::move(component)).second;
if (m_on_construct_hooks.contains(get_type_id<Component_T>()))
{
m_on_construct_hooks[get_type_id<Component_T>()](*this, entity);
}
return added_component;
};
template<typename Component_T>
void remove(EntityId entity)
{
if (m_on_destruct_hooks.contains(get_type_id<Component_T>()))
{
m_on_destruct_hooks[get_type_id<Component_T>()](*this, entity);
}
auto &derived_set = get_derived_set<Component_T>();
derived_set.remove(entity);
}
template<typename Component_T>
auto view() -> SparseSet<Component_T, EntityId> &
{
return get_derived_set<Component_T>();
};
template<typename ComponentA_T, typename ComponentB_T>
requires(!std::is_same_v<ComponentA_T, ComponentB_T>)
auto view() -> std::vector<std::tuple<EntityId, ComponentA_T &, ComponentB_T &>>
{
auto &set_a = get_derived_set<ComponentA_T>();
auto &set_b = get_derived_set<ComponentB_T>();
auto view = std::vector<std::tuple<EntityId, ComponentA_T &, ComponentB_T &>> {};
/* iterate over the "smaller" component-set, and check if its entities have the other
* component */
if (set_a.get_size() > set_b.get_size())
{
for (auto &[entity, component_b] : set_b)
{
if (set_a.contains(entity))
{
view.emplace_back(std::tie(entity, set_a.at(entity).second, component_b));
}
}
}
else
{
for (auto &[entity, component_a] : set_a)
{
if (set_b.contains(entity))
{
view.emplace_back(std::tie(entity, component_a, set_b.at(entity).second));
}
}
}
return view;
};
template<typename Component_T>
void each(std::function<void(EntityId, Component_T &)> functor)
{
for (auto &[entity, component] : get_derived_set<Component_T>())
{
functor(entity, component);
}
};
template<typename ComponentA_T, typename ComponentB_T>
requires(!std::is_same_v<ComponentA_T, ComponentB_T>)
void each(std::function<void(EntityId, ComponentA_T &, ComponentB_T &)> functor)
{
auto &set_a = get_derived_set<ComponentA_T>();
auto &set_b = get_derived_set<ComponentB_T>();
/* iterate over the "smaller" component-set, and check if its entities have the other
* component */
if (set_a.get_size() > set_b.get_size())
{
for (auto &[entity, component_b] : set_b)
{
if (set_a.contains(entity))
{
functor(entity, set_a.at(entity).second, component_b);
}
}
return;
}
for (auto &[entity, component_a] : set_a)
{
if (set_b.contains(entity))
{
functor(entity, component_a, set_b.at(entity).second);
}
}
};
[[nodiscard]] auto get_entity_count() const -> size_t
{
return static_cast<size_t>(m_entity_count);
}
private:
using TypeId = size_t;
static consteval auto hash_cstr(const char *str) -> TypeId
{
constexpr auto fnv_offset_basis = size_t { 14695981039346656037ull };
constexpr auto fnv_prime = size_t { 1099511628211ull };
auto hash = fnv_offset_basis;
for (const auto &ch : std::string_view { str })
{
hash *= fnv_prime;
hash ^= static_cast<uint8_t>(ch);
}
return hash;
}
template<typename T>
static consteval auto get_type_id() -> TypeId
{
#if defined _MSC_VER
#define GENERATOR_PRETTY_FUNCTION __FUNCSIG__
#elif defined __clang__ || (defined __GNUC__)
#define GENERATOR_PRETTY_FUNCTION __PRETTY_FUNCTION__
#else
#error "Compiler not supported"
#endif
constexpr auto value = hash_cstr(GENERATOR_PRETTY_FUNCTION);
#undef GENERATOR_PRETTY_FUNCTION
return value;
}
template<typename T>
auto get_derived_set() -> SparseSet<T, EntityId> &
{
constexpr auto type_id = get_type_id<T>();
if (!m_sparsed_sets.contains(type_id))
{
m_sparsed_sets[type_id] = memory::create_scope<SparseSet<T, EntityId>>();
}
auto *base_set = m_sparsed_sets[type_id].get();
auto *derived_set = dynamic_cast<SparseSet<T, EntityId> *>(base_set);
ensure(derived_set, "Failed to downcast to derived set");
return *derived_set;
}
EntityId m_current;
TypeId m_entity_count;
std::flat_map<TypeId, memory::Scope<UnderlyingSparseSet_T>> m_sparsed_sets;
std::flat_map<TypeId, Callback_T> m_on_construct_hooks;
std::flat_map<TypeId, Callback_T> m_on_destruct_hooks;
};
} // namespace lt::ecs

View file

@ -0,0 +1,65 @@
#pragma once
#include <ecs/components/transform.hpp>
#include <ecs/uuid.hpp>
#include <entt/entt.hpp>
#include <functional>
namespace lt {
class Entity;
class Framebuffer;
class Scene
{
public:
template<typename... T>
auto group()
{
return m_registry.group(entt::get<T...>);
}
template<typename T>
auto view()
{
return m_registry.view<T>();
}
auto create_entity(
const std::string &name,
const TransformComponent &transform = TransformComponent()
) -> Entity;
auto get_entity_by_tag(const std::string &tag) -> Entity;
auto get_entt_registry() -> entt::registry &
{
return m_registry;
}
private:
friend class Entity;
friend class SceneSerializer;
friend class SceneHierarchyPanel;
entt::registry m_registry;
auto create_entity_with_uuid(
const std::string &name,
UUID uuid,
const TransformComponent &transform = TransformComponent()
) -> Entity;
};
namespace ecs {
using Registry = Scene;
using Entity = ::lt::Entity;
} // namespace ecs
} // namespace lt

View file

@ -0,0 +1,34 @@
#pragma once
#include <ecs/entity.hpp>
#include <ecs/scene.hpp>
namespace YAML {
class Emitter;
}
namespace lt {
class SceneSerializer
{
public:
SceneSerializer(const Ref<Scene> &scene);
void serialize(const std::string &filePath);
auto deserialize(const std::string &file_path) -> bool;
void serialize_binary(const std::string &file_path);
auto deserialize_binary(const std::string &file_path) -> bool;
private:
Ref<Scene> m_scene;
void serialize_entity(YAML::Emitter &out, Entity entity);
};
} // namespace lt

Some files were not shown because too many files have changed in this diff Show more