Compare commits

..

2 commits

Author SHA1 Message Date
5dba19b9a4 ci: add daily amd64 clang fuzz ci check to .drone.yml
Some checks reported errors
continuous-integration/drone/pr Build was killed
2025-08-19 17:30:50 +03:30
0396303f62 ci: add amd64/clang/fuzz.sh 2025-08-19 17:30:50 +03:30
314 changed files with 52685 additions and 11223 deletions

View file

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

View file

@ -1,3 +1,18 @@
---
kind: pipeline
type: docker
name: daily — amd64 — clang
trigger:
branch:
- main
steps:
- name: fuzz
image: amd64_clang_fuzz:latest
pull: if-not-exists
commands:
- ./tools/ci/amd64/clang/fuzz.sh
--- ---
kind: pipeline kind: pipeline
type: exec type: exec
@ -25,13 +40,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 +61,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 +70,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,83 +91,14 @@ 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
type: docker
name: documentation — development
node:
environment: ryali
trigger:
branch:
- main
steps:
- name: build and deploy
image: documentation:latest
pull: if-not-exists
commands:
- pwd
- cd docs
- mkdir generated
- touch generated/changelogs.rst
- touch generated/api.rst
- sphinx-build -M html . .
- rm -rf /light_docs_dev/*
- mv ./html/* /light_docs_dev/
---
kind: pipeline
type: docker
name: documentation — production
node:
environment: ryali
trigger:
event:
- tag
steps:
- name: build and deploy
image: documentation:latest
pull: if-not-exists
commands:
- cd docs
- mkdir generated
- touch generated/changelogs.rst
- touch generated/api.rst
- sphinx-build -M html . .
- rm -rf /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

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,7 +1,6 @@
#pragma once #pragma once
#include <app/application.hpp> #include <app/application.hpp>
#include <memory/scope.hpp>
auto main(int argc, char *argv[]) -> int32_t auto main(int argc, char *argv[]) -> int32_t
try try
@ -9,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)
{ {

View file

@ -1,89 +1,7 @@
#pragma once #pragma once
#include <chrono>
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_dbg("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:
@ -103,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,5 +1,68 @@
#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>
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
@ -18,16 +81,12 @@ 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;

View file

@ -1,64 +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 <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);
log_trc(
"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
log_dbg("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

@ -10,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()

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

View file

@ -1,173 +0,0 @@
#pragma once
namespace lt::ecs {
/**
*
* @ref https://programmingpraxis.com/2012/03/09/sparse-sets/
*/
template<typename Identifier_T = uint32_t>
class TypeErasedSparseSet
{
public:
TypeErasedSparseSet() = default;
TypeErasedSparseSet(TypeErasedSparseSet &&) = default;
TypeErasedSparseSet(const TypeErasedSparseSet &) = default;
auto operator=(TypeErasedSparseSet &&) -> TypeErasedSparseSet & = default;
auto operator=(const TypeErasedSparseSet &) -> TypeErasedSparseSet & = default;
virtual ~TypeErasedSparseSet() = default;
virtual void remove(Identifier_T identifier) = 0;
};
template<typename Value_T, typename Identifier_T = uint32_t>
class SparseSet: public TypeErasedSparseSet<Identifier_T>
{
public:
using Dense_T = std::pair<Identifier_T, Value_T>;
static constexpr auto max_capacity = size_t { 1'000'000 };
static constexpr auto null_identifier = std::numeric_limits<Identifier_T>().max();
explicit SparseSet(size_t initial_capacity = 1)
{
ensure(
initial_capacity <= max_capacity,
"Failed to create SparseSet: capacity too large ({} > {})",
initial_capacity,
max_capacity
);
m_dense.reserve(initial_capacity);
m_sparse.resize(initial_capacity, null_identifier);
}
auto insert(Identifier_T identifier, Value_T value) -> Dense_T &
{
if (m_sparse.size() < identifier + 1)
{
auto new_capacity = std::max(static_cast<size_t>(identifier + 1), m_sparse.size() * 2);
new_capacity = std::min(new_capacity, max_capacity);
// log_dbg("Increasing sparse vector size:", m_dead_count);
// log_dbg("\tdead_count: {}", m_dead_count);
// log_dbg("\talive_count: {}", m_alive_count);
// log_dbg("\tsparse.size: {} -> {}", m_sparse.size(), new_capacity);
m_sparse.resize(new_capacity, null_identifier);
}
++m_alive_count;
m_sparse[identifier] = m_dense.size();
return m_dense.emplace_back(identifier, std::move(value));
}
/** @warn invalidates begin/end iterators
*
* @todo(Light): make it not invalidate the iterators >:c
*/
void remove(Identifier_T identifier) override
{
auto &idx = m_sparse[identifier];
auto &[entity, component] = m_dense[idx];
auto &[last_entity, last_component] = m_dense.back();
auto &last_idx = m_sparse[last_entity];
// removed entity is in dense's back, just pop and invalidate sparse[identifier]
if (entity == last_entity)
{
idx = null_identifier;
m_dense.pop_back();
}
else
{
// swap dense's 'back' to 'removed'
std::swap(component, last_component);
entity = last_entity;
// make sparse point to new idx
last_idx = idx;
// pop dense and invalidate sparse[identifier]
idx = null_identifier;
m_dense.pop_back();
}
++m_dead_count;
--m_alive_count;
}
void clear()
{
m_dense.clear();
m_sparse.clear();
m_alive_count = 0;
}
[[nodiscard]] auto contains(Identifier_T identifier) const -> bool
{
return m_sparse.size() > identifier //
&& m_sparse[identifier] != null_identifier //
&& m_dense[m_sparse[identifier]].first == identifier;
}
auto begin() -> std::vector<Dense_T>::iterator
{
return m_dense.begin();
}
auto end() -> std::vector<Dense_T>::iterator
{
return m_dense.end();
}
[[nodiscard]] auto at(Identifier_T identifier) const -> const Dense_T &
{
return m_dense.at(m_sparse.at(identifier));
}
[[nodiscard]] auto at(Identifier_T identifier) -> Dense_T &
{
return m_dense.at(m_sparse.at(identifier));
}
/** @warn unsafe, for bound-checked access: use `.at` */
[[nodiscard]] auto &&operator[](this auto &&self, Identifier_T identifier)
{
using Self_T = decltype(self);
return std::forward<Self_T>(self).m_dense[std::forward<Self_T>(self).m_sparse[identifier]];
}
[[nodiscard]] auto get_size() const noexcept -> size_t
{
return m_alive_count;
}
[[nodiscard]] auto get_capacity() const noexcept -> size_t
{
return m_sparse.capacity();
}
[[nodiscard]] auto is_empty() const noexcept -> bool
{
return m_dense.empty();
}
private:
std::vector<Dense_T> m_dense;
std::vector<Identifier_T> m_sparse;
size_t m_alive_count {};
size_t m_dead_count {};
};
} // namespace lt::ecs

View file

@ -0,0 +1,38 @@
#pragma once
#include <random>
namespace lt {
class UUID
{
public:
UUID(uint64_t uuid = -1);
operator uint64_t() const
{
return m_uuid;
}
private:
static std::mt19937_64 s_engine;
static std::uniform_int_distribution<uint64_t> s_distribution;
uint64_t m_uuid;
};
} // namespace lt
namespace std {
template<>
struct hash<lt::UUID>
{
std::size_t operator()(const lt::UUID &uuid) const
{
return hash<uint64_t>()(static_cast<uint64_t>(uuid));
}
};
} // namespace std

View file

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

View file

@ -1,68 +0,0 @@
#pragma once
namespace lt {
enum class Platform : uint8_t
{
/** The GNU/Linux platform.
* Tested on the following distros: arch-x86_64
* @note: Named like so because `linux` is a built-in identifier.
* */
gnu_linux,
/**
* The Microsoft Windows(tm) platform.
* Tested on the following architectures: x86_64
*/
windows,
/**
* The apple's macOS platform.
* Currently not supported.
*/
mac,
};
/** The compiler that was used for compiling the project. */
enum class Compiler : uint8_t
{
clang,
gcc,
msvc,
apple_clang,
};
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)
constexpr auto platform = Platform::gnu_linux;
constexpr auto platform_name = "gnu_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
#ifdef __clang__
constexpr auto compiler = Compiler::clang;
constexpr auto compiler_name = "clang";
/** @todo(Light): insert the full identifier, including version information and such */
constexpr auto full_compiler_identifier = "clang";
#endif
} // namespace constants
} // namespace lt

View file

@ -1,4 +1,2 @@
add_library_module(input system.cpp) add_library_module(input input.cpp)
target_link_libraries(input PUBLIC surface math logger tbb) target_link_libraries(input PUBLIC surface math imgui::imgui logger)
add_test_module(input system.test.cpp)

View file

@ -0,0 +1,159 @@
#include <imgui.h>
#include <input/input.hpp>
#include <logger/logger.hpp>
namespace lt {
Input::Input(): m_mouse_position {}, m_mouse_delta {}
{
restart_input_state();
}
void Input::receive_user_interface_events_impl(bool receive, bool toggle /* = false */)
{
m_user_interface_events = toggle ? !m_user_interface_events : receive;
}
void Input::receieve_game_events_impl(bool receive, bool toggle /*= false*/)
{
auto prev = m_game_events;
m_game_events = toggle ? !m_user_interface_events : receive;
if (m_game_events != prev)
{
restart_input_state();
}
}
void Input::restart_input_state()
{
m_keyboad_keys.fill(false);
m_mouse_buttons.fill(false);
m_mouse_position = math::vec2(0.0f);
m_mouse_delta = math::vec2(0.0f);
m_mouse_wheel_delta = 0.0f;
}
void Input::on_event(const Event &inputEvent)
{
auto &io = ImGui::GetIO();
switch (inputEvent.get_event_type())
{
//** MOUSE_EVENTS **//
case EventType::MouseMoved:
{
const auto &event = dynamic_cast<const MouseMovedEvent &>(inputEvent);
if (m_game_events)
{
m_mouse_delta = event.get_position() - m_mouse_position;
m_mouse_position = event.get_position();
}
if (m_user_interface_events)
{
io.MousePos = ImVec2(event.get_x(), event.get_y());
}
return;
}
case EventType::ButtonPressed:
{
const auto &event = dynamic_cast<const ButtonPressedEvent &>(inputEvent);
if (m_game_events)
{
m_mouse_buttons[event.get_button()] = true;
}
if (m_user_interface_events)
{
io.MouseDown[event.get_button()] = true;
}
return;
}
case EventType::ButtonReleased:
{
const auto &event = dynamic_cast<const ButtonReleasedEvent &>(inputEvent);
if (m_game_events)
{
m_mouse_buttons[event.get_button()] = false;
}
if (m_user_interface_events)
{
io.MouseDown[event.get_button()] = false;
}
return;
}
case EventType::WheelScrolled:
{
const auto &event = dynamic_cast<const WheelScrolledEvent &>(inputEvent);
if (m_game_events)
{
m_mouse_wheel_delta = event.get_offset();
}
if (m_user_interface_events)
{
io.MouseWheel = event.get_offset();
}
return;
}
//** KEYBOARD_EVENTS **//
case EventType::KeyPressed:
{
const auto &event = dynamic_cast<const KeyPressedEvent &>(inputEvent);
if (m_game_events)
{
m_keyboad_keys[event.get_key()] = true;
}
if (m_user_interface_events)
{
// io.AddKeyEvent(event.get_key(), true);
// if (event.get_key() == Key::BackSpace)
// io.AddInputCharacter(Key::BackSpace);
}
return;
}
case EventType::KeyReleased:
{
const auto &event = dynamic_cast<const KeyReleasedEvent &>(inputEvent);
if (m_game_events)
{
m_keyboad_keys[event.get_key()] = false;
}
if (m_user_interface_events)
{
// io.AddKeyEvent(event.get_key(), false);
}
return;
}
case EventType::SetChar:
{
if (m_user_interface_events)
{
const auto &event = dynamic_cast<const SetCharEvent &>(inputEvent);
io.AddInputCharacter(event.get_character());
}
return;
}
default: log_trc("Dropped event");
}
}
} // namespace lt

View file

@ -1,145 +1,5 @@
#include <input/components.hpp>
#include <input/system.hpp> #include <input/system.hpp>
#include <memory/reference.hpp>
namespace lt::input { namespace lt::input {
template<class... Ts>
struct overloads: Ts...
{
using Ts::operator()...;
};
System::System(memory::Ref<ecs::Registry> registry): m_registry(std::move(registry))
{
ensure(m_registry, "Failed to initialize input system: null registry");
}
void System::tick(app::TickInfo tick)
{
for (auto &[entity, surface] : m_registry->view<surface::SurfaceComponent>())
{
for (const auto &event : surface.peek_events())
{
handle_event(event);
}
}
for (auto &[entity, input] : m_registry->view<InputComponent>())
{
// TODO(Light): instead of iterating over all actions each frame,
// make a list of "dirty" actions to reset
// and a surface_input->input_action mapping to get to action through input
// instead of brute-force checking all of them.
for (auto &action : input.m_actions)
{
auto code = action.trigger.mapped_keycode;
if (code < m_keys.size() && m_keys[code])
{
if (action.state == InputAction::State::triggered)
{
action.state = InputAction::State::active;
}
else if (action.state == InputAction::State::inactive)
{
action.state = InputAction::State::triggered;
}
}
else
{
action.state = InputAction::State::inactive;
}
}
}
const auto now = std::chrono::steady_clock::now();
m_last_tick_result = app::TickResult {
.info = tick,
.duration = now - tick.start_time,
.end_time = now,
};
}
void System::on_register()
{
}
void System::on_unregister()
{
}
void System::handle_event(const surface::SurfaceComponent::Event &event)
{
const auto visitor = overloads {
[this](const surface::ClosedEvent &) { on_surface_lost_focus(); },
[this](const surface::LostFocusEvent &) { on_surface_lost_focus(); },
[this](const surface::KeyPressedEvent &event) { on_key_press(event); },
[this](const surface::KeyReleasedEvent &event) { on_key_release(event); },
[this](const surface::MouseMovedEvent &event) { on_pointer_move(event); },
[this](const surface::ButtonPressedEvent &event) { on_button_press(event); },
[this](const surface::ButtonReleasedEvent &event) { on_button_release(event); },
[this](auto) {},
};
std::visit(visitor, event);
}
void System::on_surface_lost_focus()
{
for (auto &key : m_keys)
{
key = false;
}
for (auto &button : m_buttons)
{
button = false;
}
}
void System::on_key_press(const lt::surface::KeyPressedEvent &event)
{
if (event.get_key() > m_keys.size())
{
log_dbg(
"Key code larger than key container size, implement platform-dependant "
"key-code-mapping!"
);
return;
}
m_keys[event.get_key()] = true;
}
void System::on_key_release(const lt::surface::KeyReleasedEvent &event)
{
if (event.get_key() > m_keys.size())
{
log_dbg(
"Key code larger than key container size, implement platform-dependant "
"key-code-mapping!"
);
return;
}
m_keys[event.get_key()] = false;
}
void System::on_pointer_move(const lt::surface::MouseMovedEvent &event)
{
m_pointer_position = event.get_position();
}
void System::on_button_press(const lt::surface::ButtonPressedEvent &event)
{
m_buttons[event.get_button()] = true;
}
void System::on_button_release(const lt::surface::ButtonReleasedEvent &event)
{
m_buttons[event.get_button()] = false;
}
} // namespace lt::input } // namespace lt::input

View file

@ -1,202 +0,0 @@
#include <ecs/entity.hpp>
#include <input/components.hpp>
#include <input/system.hpp>
#include <memory/reference.hpp>
#include <memory/scope.hpp>
#include <ranges>
#include <surface/system.hpp>
#include <test/test.hpp>
// NOLINTBEGIN
using namespace lt;
using input::InputComponent;
using input::System;
using std::ignore;
using test::Case;
using test::expect_eq;
using test::expect_false;
using test::expect_ne;
using test::expect_not_nullptr;
using test::expect_throw;
using test::Suite;
// NOLINTEND
[[nodiscard]] auto tick_info() -> app::TickInfo
{
return {
.delta_time = std::chrono::milliseconds { 16 },
.budget = std::chrono::milliseconds { 10 },
.start_time = std::chrono::steady_clock::now(),
};
}
class Fixture
{
public:
[[nodiscard]] auto registry() -> memory::Ref<ecs::Registry>
{
return m_registry;
}
auto add_input_component() -> ecs::EntityId
{
auto entity = m_registry->create_entity();
m_registry->add<InputComponent>(entity, {});
return entity;
}
auto add_surface_component() -> ecs::EntityId
{
auto entity = m_registry->create_entity();
m_surface_system.create_surface_component(
entity,
{ .title = "", .resolution = { 20u, 20u } }
);
return entity;
}
private:
memory::Ref<ecs::Registry> m_registry = memory::create_ref<ecs::Registry>();
surface::System m_surface_system = surface::System { m_registry };
};
Suite raii = "raii"_suite = "raii"_suite = [] {
Case { "happy path won't throw" } = [&] {
System { Fixture {}.registry() };
};
Case { "many won't freeze/throw" } = [&] {
auto fixture = Fixture {};
for (auto idx : std::views::iota(0, 10'000))
{
ignore = System { fixture.registry() };
}
};
Case { "unhappy path throws" } = [] {
expect_throw([] { ignore = System { {} }; });
};
};
Suite system_events = "system_events"_suite = [] {
Case { "on_register won't throw" } = [] {
auto fixture = Fixture {};
auto registry = fixture.registry();
auto system = System { registry };
system.on_register();
expect_eq(registry->view<InputComponent>().get_size(), 0);
};
Case { "on_unregister won't throw" } = [] {
auto fixture = Fixture {};
auto registry = fixture.registry();
auto system = System { registry };
system.on_register();
system.on_unregister();
expect_eq(registry->view<InputComponent>().get_size(), 0);
};
};
Suite registry_events = "registry_events"_suite = [] {
Case { "on_construct<InputComnent>" } = [] {
auto fixture = Fixture {};
auto registry = fixture.registry();
auto system = System { registry };
const auto &entity = fixture.add_input_component();
expect_eq(registry->view<InputComponent>().get_size(), 1);
};
Case { "on_destrroy<InputComponent>" } = [] {
auto fixture = Fixture {};
auto registry = fixture.registry();
auto system = memory::create_scope<System>(registry);
auto entity_a = fixture.add_input_component();
auto entity_b = fixture.add_input_component();
expect_eq(registry->view<InputComponent>().get_size(), 2);
registry->remove<InputComponent>(entity_a);
expect_eq(registry->view<InputComponent>().get_size(), 1);
system.reset();
expect_eq(registry->view<InputComponent>().get_size(), 1);
registry->remove<InputComponent>(entity_b);
expect_eq(registry->view<InputComponent>().get_size(), 0);
};
};
Suite tick = "tick"_suite = [] {
Case { "Empty tick won't throw" } = [] {
auto fixture = Fixture {};
auto registry = fixture.registry();
auto system = System { fixture.registry() };
system.tick(tick_info());
};
Case { "Tick triggers input action" } = [] {
auto fixture = Fixture {};
auto registry = fixture.registry();
auto system = System { fixture.registry() };
auto surface_entity = fixture.add_surface_component();
auto &surface = registry->get<surface::SurfaceComponent>(surface_entity);
auto input_entity = fixture.add_input_component();
auto &input = registry->get<InputComponent>(input_entity);
auto action_key = input.add_action(
{
.name { "test" },
.trigger = { .mapped_keycode = 69 },
}
);
expect_eq(input.get_action(action_key).state, input::InputAction::State::inactive);
system.tick(tick_info());
expect_eq(input.get_action(action_key).state, input::InputAction::State::inactive);
surface.push_event(surface::KeyPressedEvent(69));
system.tick(tick_info());
expect_eq(input.get_action(action_key).state, input::InputAction::State::triggered);
system.tick(tick_info());
expect_eq(input.get_action(action_key).state, input::InputAction::State::active);
system.tick(tick_info());
system.tick(tick_info());
system.tick(tick_info());
expect_eq(input.get_action(action_key).state, input::InputAction::State::active);
surface.push_event(surface::KeyReleasedEvent(69));
system.tick(tick_info());
expect_eq(input.get_action(action_key).state, input::InputAction::State::inactive);
};
Case { "Tick triggers" } = [] {
auto fixture = Fixture {};
auto registry = fixture.registry();
auto system = System { fixture.registry() };
auto surface_entity = fixture.add_surface_component();
auto &surface = registry->get<surface::SurfaceComponent>(surface_entity);
auto input_entity = fixture.add_input_component();
auto &input = registry->get<InputComponent>(input_entity);
auto action_key = input.add_action(
{
.name { "test" },
.trigger = { .mapped_keycode = 69 },
}
);
};
};

View file

@ -1,57 +0,0 @@
#pragma once
#include <vector>
namespace lt::input {
struct Trigger
{
uint32_t mapped_keycode;
};
struct InputAction
{
using Key = size_t;
enum class State : uint8_t
{
inactive,
active,
triggered,
cancelled,
};
std::string name;
State state;
Trigger trigger;
};
class InputComponent
{
public:
InputComponent() = default;
auto add_action(InputAction action) -> size_t
{
m_actions.emplace_back(std::move(action));
return m_actions.size() - 1;
}
auto get_action(auto idx) -> const InputAction &
{
return m_actions[idx];
}
private:
friend class System;
void push_event()
{
}
std::vector<InputAction> m_actions;
};
} // namespace lt::input

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