Compare commits

...
Sign in to create a new pull request.

45 commits

Author SHA1 Message Date
4a7a220af8
fix: missing display env var
Some checks are pending
continuous-integration/drone/push Build is running
2025-09-05 12:21:44 +03:30
a43243ca3f
build: differentiate between test types
Some checks are pending
continuous-integration/drone/push Build is running
fix: #41
2025-09-05 12:15:40 +03:30
5d30a56e22
revert: uncommenting the whole .drone.yml
Some checks are pending
continuous-integration/drone/push Build is running
2025-09-05 11:36:29 +03:30
51e044065a ci: documentation pipelines (#39)
All checks were successful
continuous-integration/drone/push Build is passing
fix: #36
fix: #37
Co-authored-by: light7734 <light7734@tuta.io>
Co-committed-by: light7734 <light7734@tuta.io>
2025-08-23 11:36:02 +00:00
b0caeded2a
fix(surface): invalid value_or invokation for libc++
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-08-12 15:12:17 +03:30
585d37b31b
test: add fuzz seed for surface
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-08-11 23:06:33 +03:30
4cd258bcb6
refactor(test): printable concept to accept enums
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-08-10 23:26:06 +03:30
052ac6dd5b
docs(guidelines/conventions): minor changes
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone Build was killed
2025-08-06 12:18:51 +03:30
b005857c31
docs(architecture/resource): add missing header lines 2025-08-06 12:18:36 +03:30
9389dfe7fb
docs: add features.rst & showcase.rst for light 2025-08-06 12:18:13 +03:30
40503239df
docs: setup toctree 2025-08-06 12:17:21 +03:30
552602f0af
docs: update generate_changelog.py 2025-08-06 12:16:29 +03:30
76fc5dd572
chore: add docs/generated to docs/.gitignore file 2025-08-06 12:15:43 +03:30
cd571d4a9d
docs(guidelines/development): add missing commit types 2025-08-06 12:13:31 +03:30
813e8a3a3a
docs(guidelines/development): add semantic versioning 2025-08-06 12:13:07 +03:30
d6aa5fc91d
docs: initial guidelines/philosophy content 2025-08-06 12:11:59 +03:30
bd2f74b120
docs: minor changes to architecture/assets 2025-08-06 12:11:03 +03:30
af4ce09838 docs: add generate_changelog.py script
for generating changelogs :D
2025-08-06 12:10:13 +03:30
f7591a23f4
chore: add docs/.gitignore 2025-08-06 12:07:52 +03:30
51990599a7
docs: initial revision
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-08-05 23:42:08 +03:30
459b3b961d
feat(renderer/vk): function loading & device creation
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-08-05 10:39:08 +03:30
d58f8994aa
refactor(debug): fix & improve ensure
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-07-31 08:42:42 +03:30
c57e5a56ac
fix(test): process_fuzz_input returning EXIT_SUCCESS on non-zero harness returns
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-07-31 08:13:58 +03:30
ea8986b764
fix(mirror): typo 2025-07-31 08:13:09 +03:30
e36991e6de
test(surface): add fuzz testing
test(surface): add & fix unit tests

fix(surface): bugs

refactor(surface): minor refactors & some edge-case handling
2025-07-31 08:11:05 +03:30
60ad7cdc70
feat(test): add fuzz testing support
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-07-30 23:02:53 +03:30
638a009047
refactor: surface, app, tests, ecs refactors
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-07-28 20:45:24 +03:30
a102db0699
refactor: minor adjustments to test log formattings
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-07-27 22:52:38 +03:30
2b96a85b62
feat: systems
Some checks reported errors
continuous-integration/drone/push Build was killed
feat: surface system

This commit puts the project in major jeopardy as it overhauls the
architecture such as removing the layer stack completely, etc.

I am filled with determination.
2025-07-26 18:01:27 +03:30
d9229ad912 build: test executable target not having private include dirs of target lib
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-07-25 15:07:43 +03:30
6a814bd177 style: minor fixes 2025-07-25 15:07:13 +03:30
2d019878a5 tests: add missing test cases for test/expects 2025-07-25 15:06:44 +03:30
b0ad9ff964 feat(test): expect_unreachable
feat(test): expect_throw
2025-07-25 15:06:14 +03:30
22c62bf5f9
docs: add initial guidelines/conventions
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-07-24 22:42:44 +03:30
d83e269432
refactor: move renderer gl/dx files to private section
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-07-23 10:11:55 +03:30
65f0d3bb73
fuck: fuck!
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-07-22 23:51:22 +03:30
0b94aaffa7
ci: add libc++ to coverage.dockerfile
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-21 19:19:39 +03:30
026f97ad0b
ci: dirty fix
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-21 19:06:37 +03:30
8720fdcebf
ci: fix llvm-cov -ignore-filename-regex pattern
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-21 18:56:20 +03:30
46505a6c24
ci: fix coverage.sh including external
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-21 18:36:58 +03:30
754b6361ad
ci: fix llvm-cov not taking into account symbol library
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-21 18:30:51 +03:30
bf485e354a
fix: amd64/clang/coverage.sh
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-21 16:19:04 +03:30
688c88f255 ci: add clang code coverage check (#10)
All checks were successful
continuous-integration/drone/push Build is passing
reviewed-on: #10
Co-authored-by: light7734 <light7734@tuta.io>
Co-committed-by: light7734 <light7734@tuta.io>
2025-07-21 09:37:44 +00:00
3998c4127a ci: add memory sanitization check (#9)
All checks were successful
continuous-integration/drone/push Build is passing
reviewed-on: #9
Co-authored-by: light7734 <light7734@tuta.io>
Co-committed-by: light7734 <light7734@tuta.io>
2025-07-20 11:12:29 +00:00
798732632a ci: major refactors & add msvc check (#8)
Some checks failed
continuous-integration/drone/push Build is failing
reviewed-on: #8
Co-authored-by: light7734 <light7734@tuta.io>
Co-committed-by: light7734 <light7734@tuta.io>
2025-07-20 04:37:05 +00:00
129 changed files with 3482 additions and 2082 deletions

View file

@ -1,7 +1,7 @@
---
kind: pipeline
type: exec
name: macrohard doors
name: amd64 — msvc
trigger:
branch:
- main
@ -9,107 +9,62 @@ platform:
os: windows
arch: amd64
clone:
disable: true
steps:
- name: clone
environment:
HOME: C:\Users\username\
- name: greeting
commands:
- echo 'Hello from Windows 10! :D'
- name: unit tests
shell: powershell
commands:
- ./tools/ci/amd64/msvc/unit_tests.ps1
---
kind: pipeline
type: docker
name: unit tests
name: amd64 — gcc
trigger:
branch:
- main
steps:
- name: unit tests
image: unit_tests:latest
image: amd64_gcc_unit_tests:latest
pull: if-not-exists
commands:
- |
set -e
- ./tools/ci/amd64/gcc/unit_tests.sh
conan build . \
-c tools.system.package_manager:mode=install \
-c tools.cmake.cmaketoolchain:generator=Ninja \
-s build_type=Release \
-o enable_static_analysis=False \
-o enable_tests=True \
-o use_mold=True \
--build=missing
for test in $(find ./build -type f -name '*_tests' -executable); do
echo "Running $test"
"$test"
done
---
kind: pipeline
type: docker
name: valgrind
trigger:
branch:
- main
steps:
- name: valgrind
image: valgrind:latest
image: amd64_gcc_valgrind:latest
pull: if-not-exists
commands:
- |
set -e
conan build . \
-c tools.system.package_manager:mode=install \
-c tools.cmake.cmaketoolchain:generator=Ninja \
-s build_type=Release \
-o enable_static_analysis=False \
-o enable_tests=True \
-o use_mold=True \
--build=missing
find ./build -type f -name "*_tests" -executable | xargs -I {} bash -c 'valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --verbose --error-exitcode=255 {}' || exit 1
- ./tools/ci/amd64/gcc/valgrind.sh
---
kind: pipeline
type: docker
name: leak sanitizer
name: amd64 — clang
trigger:
branch:
- main
steps:
- name: code coverage
image: amd64_clang_coverage:latest
pull: if-not-exists
environment:
CODECOV_TOKEN:
from_secret: CODECOV_TOKEN
commands:
- ./tools/ci/amd64/clang/coverage.sh
- name: leak sanitizer
image: leak_sanitizer:latest
image: amd64_clang_lsan:latest
pull: if-not-exists
commands:
- |
set -e
- ./tools/ci/amd64/clang/lsan.sh
conan build . \
-c tools.system.package_manager:mode=install \
-c tools.cmake.cmaketoolchain:generator=Ninja \
-c tools.build:cxxflags='["-g", "-fno-omit-frame-pointer", "-nostdinc++", "-isystem", "/libcxx_lsan/include/c++/v1/", "-fsanitize=leak"]' \
-c tools.build:sharedlinkflags='["-L/libcxx_lsan/lib", "-Wl,-rpath,/libcxx_lsan/lib", "-lc++", "-lc++abi", "-fsanitize=leak"]' \
-c tools.build:exelinkflags='["-L/libcxx_lsan/lib", "-Wl,-rpath,/libcxx_lsan/lib", "-lc++", "-lc++abi", "-fsanitize=leak"]' \
-c tools.info.package_id:confs='["tools.build:cxxflags","tools.build:sharedlinkflags","tools.build:exelinkflags"]' \
-c tools.build:compiler_executables='{"c": "clang", "cpp": "clang++"}' \
-s build_type=Release \
-s compiler=clang \
-s compiler.version=20 \
-s compiler.libcxx=libc++ \
-o use_mold=True \
--build=missing
for test in $(find ./build -type f -name '*_tests' -executable); do
echo "Running $test"
"$test"
done
- name: memory sanitizer
image: amd64_clang_msan:latest
pull: if-not-exists
commands:
- ./tools/ci/amd64/clang/msan.sh
---
kind: pipeline
@ -120,49 +75,66 @@ trigger:
- main
steps:
- name: static_analysis
image: static_analysis:latest
- name: clang tidy
image: clang_tidy:latest
pull: if-not-exists
privileged: true
commands:
- |
conan build . \
-c tools.system.package_manager:mode=install \
-c tools.cmake.cmaketoolchain:generator=Ninja \
-s build_type=Release \
-o enable_static_analysis=True \
-o enable_tests=True \
-o use_mold=True \
--build=missing
- ./tools/ci/static_analysis/clang_tidy.sh
- name: clang format
image: clang_format:latest
pull: if-not-exists
commands:
- ./tools/ci/static_analysis/clang_format.sh
---
kind: pipeline
type: docker
name: clang format
type: docker
name: documentation — development
node:
environment: ryali
trigger:
branch:
- main
steps:
- name: clang format
image: clang_format:latest
- name: build and deploy
image: documentation:latest
pull: if-not-exists
commands:
- |
set -e
clang-format --version
has_fomatting_issues=0
- pwd
- cd docs
- mkdir generated
- touch generated/changelogs.rst
- touch generated/api.rst
- sphinx-build -M html . .
for file in $(find ./modules -name '*.?pp'); do
echo "Checking format for $file"
if ! clang-format --dry-run --Werror "$file"; then
echo "❌ Formatting issue detected in $file"
has_fomatting_issues=1
fi
done
- rm -rf /light_docs_dev/*
- mv ./html/* /light_docs_dev/
if [ "$has_fomatting_issues" -eq 0 ]; then
echo "✅ All files are properly formatted! Well done! ^~^"
fi
---
exit ${has_fomatting_issues}
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:
- pwd
- 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,16 +1,43 @@
cmake_minimum_required(VERSION 3.14)
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(${CMAKE_CURRENT_SOURCE_DIR}/tools/cmake/definitions.cmake)
include(${CMAKE_CURRENT_SOURCE_DIR}/tools/cmake/dependencies.cmake)
include(CheckCXXSourceCompiles)
include(${CMAKE_DIR}/functions.cmake)
include(${CMAKE_DIR}/definitions.cmake)
include(${CMAKE_DIR}/dependencies.cmake)
add_option(ENABLE_STATIC_ANALYSIS "Performs static analysis via clang-tidy and fails build on failing checks")
add_option(ENABLE_UNIT_TESTS "Enables the building of the unit test modules")
add_option(ENABLE_FUZZ_TESTS "Enables the building of the fuzz 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_TESTS "Enables the building of the test modules")
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}/external/glad)

View file

@ -1,2 +1,6 @@
# Light
See docs.light7734.com for a comprehensive project documentation
<!---FUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUCK
MEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!!!!!!!!-->

View file

@ -11,14 +11,18 @@ class LightRecipe(ConanFile):
generators = "CMakeDeps"
options = {
"enable_tests": [True, False],
"enable_unit_tests": [True, False],
"enable_fuzz_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_unit_tests": True,
"enable_fuzz_tests": False,
"enable_llvm_coverage": False,
"enable_static_analysis": False,
"use_mold": False,
"export_compile_commands": True,
@ -44,8 +48,10 @@ class LightRecipe(ConanFile):
tc.cache_variables["CMAKE_LINKER_TYPE"] = "MOLD"
tc.cache_variables["CMAKE_EXPORT_COMPILE_COMMANDS"] = self.options.export_compile_commands
tc.cache_variables["ENABLE_UNIT_TESTS"] = self.options.enable_unit_tests
tc.cache_variables["ENABLE_FUZZ_TESTS"] = self.options.enable_fuzz_tests
tc.cache_variables["ENABLE_LLVM_COVERAGE"] = self.options.enable_llvm_coverage
tc.cache_variables["ENABLE_STATIC_ANALYSIS"] = self.options.enable_static_analysis
tc.cache_variables["ENABLE_TESTS"] = self.options.enable_tests
repo = git.Repo(search_parent_directories=True)
tc.cache_variables["GIT_HASH"] = repo.head.object.hexsha

3
docs/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
_build/
generated/

View file

@ -1,62 +1,72 @@
Asset Management
===================================================================================================
Layout
On Disk (file) Layout
---------------------------------------------------------------------------------------------------
{version} | 4 bytes, ie. uint32_t
{general metadata} | sizeof(AssetMetadata)
{specialized metadata} | sizeof(XXXAssetMetadata), eg. TextureAssetMetadata
{n} | 4 bytes, ie. uint32_t
{blob_0...n metadata} | n * sizeof(BlobMetadata)
{blob_0...n data} | variable size based on actual data
{end marker} | 8 byte, ie size_t for marking the END
.. code-block:: md
{version} | 4 bytes, ie. uint32_t
{general metadata} | sizeof(AssetMetadata)
{specialized metadata} | sizeof(XXXAssetMetadata), eg. TextureAssetMetadata
{n} | 4 bytes, ie. uint32_t
{blob_0...n metadata} | n * sizeof(BlobMetadata)
{blob_0...n data} | variable size based on actual data
{end marker} | 8 byte, ie size_t for marking the END
Sections
---------------------------------------------------------------------------------------------------
version -> The version of the asset for forward compatibility
general metadata -> Common asset metadata such as file-path, asset-type, creator, etc.
specialized metadata -> Metadata specific to the asset, eg. texture dimensions for Textures.
n -> The number of blobs.
blob_0...n metadata -> Metadata specifying how the actual data is packed, required for unpacking.
blob_0...n data -> The actual data, packed and compressed to be reacdy for direct engine consumption.
.. code-block:: md
version -> The version of the asset for forward compatibility
general metadata -> Common asset metadata such as file-path, asset-type, creator, etc.
specialized metadata -> Metadata specific to the asset, eg. texture dimensions for Textures.
n -> The number of blobs.
blob_0...n metadata -> Metadata specifying how the actual data is packed, required for unpacking.
blob_0...n data -> The actual data, packed and compressed to be reacdy for direct engine consumption.
Loading
---------------------------------------------------------------------------------------------------
Each `Loader` has ONE OR MORE supported input file types (detected via the file extension): eg. StbLoader -> Can read in .jpg, .png, .bmp, etc.... files
Loading pre-baked asset files (like .png files) for baking:
Each `Loader` has ONLY ONE supported output asset type:
Each **Loader** has ONE OR MORE supported input file types (detected via the file extension): eg. StbLoader -> Can read in .jpg, .png, .bmp, etc.... files
Each **Loader** has ONLY ONE supported output asset type:
eg. StbLoader -> outputs TextureAsset
Multiple `Loader`s MAY have as output the same asset type:
Multiple **Loader**\s MAY have as output the same asset type:
eg. StbLoader -> outputs TextureAsset
eg. SomeOtherImgLoader -> outputs TextureAsset
Multiple `Loader`s SHOULD NOT have as input same extension types
eg. .jpg, .png -> if supported, should only be supported by 1 `Loader` class
Multiple **Loader**\s SHOULD NOT have as input same extension types
eg. .jpg, .png -> if supported, should only be supported by 1 **Loader** class
Each `Loader` SHOULD read and turn the data from a file (eg. .png for textures) into something
understandable by a `Packer` (not the engine itself).
Each **Loader** SHOULD read and turn the data from a file (eg. .png for textures) into something
understandable by a **Packer** (not the engine itself).
A `Loader` SHOULD NOT be responsible for packing the parsed file data into asset data,
A **Loader** SHOULD NOT be responsible for packing the parsed file data into asset data,
as that implies direct understanding of the layout required by the engine.
And if that layout changes, ALL `Loader`s should change accordingly;
And if that layout changes, ALL **Loader**s should change accordingly;
which makes a class that's responsible for reading files dependant on the engine's (potentially frequent) internal changes.
The logic is to reduce many-to-one dependency into a one-to-one dependency by redirecting the packing process to `Packer` classes
The logic is to reduce many-to-one dependency into a one-to-one dependency by redirecting the packing process to **Packer** classes
Packing
---------------------------------------------------------------------------------------------------
Each `Packer` is responsible for packing ONLY ONE asset type:
Each **Packer** is responsible for packing ONLY ONE asset type:
eg. TexturePacker for packing texture assets from parsed image files.
eg. ModelPacker for packing model assets from parsed model files.
Each `Packer` will output ONE OR MORE blobs of data,
Each **Packer** will output ONE OR MORE blobs of data,
and for EACH blob of data, it'll write a BlobMetadata, AFTER the specialized metadata (eg. TextureAssetMetadata)
A `Packer` will make use of the `Compressor` classes to compress the data,
A **Packer** will make use of the **Compressor** classes to compress the data,
and lay it out in a way that is suitable for the engine's consumption.
Unpacking
---------------------------------------------------------------------------------------------------
A `Parser` is responsible for parsing ONLY ONE asset type:
A **Parser** is responsible for parsing ONLY ONE asset type:
eg. TextureParser for parsing texture assets for direct engine consumption.
eg. ModelParser for parsing model assets for direct engine consumption.

View file

@ -1,4 +1,5 @@
Resource Management
.. architecture/resources
Resource Management
===================================================================================================

27
docs/conf.py Normal file
View file

@ -0,0 +1,27 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'light'
copyright = '2025, light7734'
author = 'light7734'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = []
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']

View file

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

View file

@ -0,0 +1,10 @@
.. guidelines/conventions
Coding Conventions
===================================================================================================
Any line of code added to the engine, must abide by following conventions.
They may seem arbitrary, and sometimes they are. But to achieve **consistency**, which is not an arbitrary goal, is to
follow these guidelines.
AAA
--------------------

View file

@ -0,0 +1,147 @@
.. guidelines/development
Development
===================================================================================================
As a solo-project, I am not only the **developer**, but also the **manager**.
Therefore there is a need, if this project is to succeed, to have a development plan.
Such a plan should:
- Define a way to **distribute work** (across time, since there's only 1 developer).
- Define what is a **unit of work** (cycles).
- Provide a way to **track productivity**, which helps projecting the future and **detecting patterns** early on.
- Provide a **pipeline** for the work to go through and **minimize ambiguity**.
These are the **management** aspects of the project, which help the development goals to be more **pragmatic**
---by pulling my mind out of its **engineering dreamland**, and make it focus on the **broader picture**.
Cycle
---------------------------------------------------------------------------------------------------
A cycle is one **step** in development, one cycle = one ticket, and it consists of 4 stages:
1 - Make it known
- Write the commit message.
- This limits the **scope of changes** and gives you a very specific **goal** to work towards.
- If something outside of this scope really bothers you, fix and stash for a future cycle.
- Make a ticket if stash-fix is implausible ---**DO NOT** write **todo** comments.
- The message should follow the project's **commit message specifications**.
- Make a ticket.
- Version control (git) is a **development-tool**, not a **management-tool**.
- Provide a very brief description ---This may be used in the commit message's body.
2 - Make it work
- Write high-level tests that confirms the cycle's requirements are met.
- That is, specify requirements in a programming language instead of English.
- You're done when all the tests pass.
- Preferably write the tests first, but it's okay to start with the interface.
- Tests may not be necessary depending on the requirements and commit type.
- "Make it work" doesn't mean liberally producing shit code, you should:
- Follow project's **conventions**.
- Follow **best practices** and **proven swe principles**.
- Enable **warnings as errors**.
- Enable **static analysis**.
- Don't break any pre-existing-tests.
- Have the over-all picture in mind.
3 - Make it right
- Test driven refactoring
- Now you have a better picture of how things relate and work.
- Switch to a TDD-style development to do the refactoring while following swe best-practices and proven-principles.
4 - Make it fast
- This is an engine, at the end of the day, **performance** is king.
- Get a performance and/or memory profile and try to alleviate the bottlenecks.
- Avoid premature optimizations, be certain what you change has performance benefits.
Sprint
---------------------------------------------------------------------------------------------------
A sprint is the collection of all the finished cycles in one week.
It's meant to provide insight on development speed and help projecting the future.
Commit Message Specification
---------------------------------------------------------------------------------------------------
The project follows the `Conventional Commits Specification <https://www.conventionalcommits.org/en/v1.0.0-beta.4>`_.
.. code-block:: md
<type>[optional scope]: <description>
[optional body]
[optional footer]
With the following commit types:
- feat
- For adding a new feature.
- Causes a **minor** bump in version.
- fix
- For changes that fix one or more bug.
- Causes a **patch** bump in version.
- refactor
- For non feat/fix changes that improve the implementation and/or the interface.
- Causes a **patch** bump in version.
- perf
- For changes that (hopefully) improve the performance.
- Causes a **patch** bump in version.
- build
- For changes that affect the build system or external dependencies.
- Causes a **patch** bump in version.
- asset
- For changes to the files under the ``/data`` directory.
- Causes a **patch** bump in version.
- test
- For adding missing tests or correcting the existing tests.
- Does not affect the version.
- chore
- For releases, .gitignore changes, deleting unused files, etc.
- Does not affect the version.
- ci
- For changes to our CI configuration files and scripts, including files under ``/tools/ci``.
- Does not affect the version.
- docs
- For changes to the documentations.
- Does not affect the version.
Semantic Versioning
---------------------------------------------------------------------------------------------------
Coupled with conventional commit style messages, we can automajically version the project following
the **Semantic Versioning 2.0.0** specifications.
The full version identifier consits of a version core (major.minor.patch) + label + hexsha of the commit.
Using the following format:
.. code-block:: md
<major>.<minor>.<patch>-<label>+<short_hexsha>
eg.
0.8.1-kitten+ea898
0.5.0-kitten+01d85
1.5.0-akasha+7de53
kitten refers to all pre-release (1.0.0) versions
The shortened hexsha of a commit is obtained by:
``git rev-parse --short=5 <commit_hexsha>``

View file

@ -0,0 +1,7 @@
.. guidelines/philosophy
Philosophy
===================================================================================================
| **A theory or attitude that acts as a guiding principle for behaviour.**
| --- Oxford Languages

View file

@ -1,6 +1,32 @@
A bleeding-edge, cross-platform, cross-graphics-api, minimal-dependencies modern game-engine.
.. light documentation
.. toctree::
:maxdepth: 2
:caption: Light Engine
light/showcase.rst
light/features.rst
.. toctree::
:maxdepth: 2
:caption: Software Architecture
architecture/assets.rst
architecture/resource.rst
.. toctree::
:maxdepth: 2
:caption: Development Guidelines
guidelines/philosophy.rst
guidelines/development.rst
guidelines/conventions.rst
.. toctree::
:maxdepth: 2
:caption: Generated Docs
generated/api.rst
generated/changelog.rst
Supported Platforms: Windows, Mac, Linux, FreeBSD
Supported GraphicsAPIs: DirectX12-Ultimate, Vulkan 1.4, Metal, OpenGL 4.6
Dependencies: stdlib, meshoptimizer

4
docs/light/features.rst Normal file
View file

@ -0,0 +1,4 @@
.. light/features
Features
===================================================================================================

4
docs/light/showcase.rst Normal file
View file

@ -0,0 +1,4 @@
.. light/demos
Showcase
===================================================================================================

35
docs/make.bat Normal file
View file

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

View file

@ -4,22 +4,21 @@ add_subdirectory(./time)
add_subdirectory(./logger)
add_subdirectory(./debug)
add_subdirectory(./math)
#
add_subdirectory(./asset_baker)
add_subdirectory(./asset_parser)
add_subdirectory(./asset_manager)
# add_subdirectory(./asset_manager)
#
add_subdirectory(./camera)
add_subdirectory(./input)
add_subdirectory(./ui)
add_subdirectory(./window)
add_subdirectory(./renderer)
# add_subdirectory(./input)
# add_subdirectory(./ui)
#
add_subdirectory(./surface)
# add_subdirectory(./renderer)
add_subdirectory(./ecs)
#
add_subdirectory(./app)
# apps
add_subdirectory(./mirror)
add_subdirectory(test)

View file

@ -1,21 +1,2 @@
add_library_module(app
application.cpp
layer.cpp
layer_stack.cpp
)
target_link_libraries(app
PUBLIC
renderer
logger
ui
asset_parser
asset_manager
lt_debug
ecs
window
glad
time
opengl::opengl
EnTT::EnTT
)
add_library_module(app application.cpp)
target_link_libraries(app PRIVATE lt_debug)

View file

@ -1,203 +1,48 @@
#include <app/application.hpp>
#include <app/layer.hpp>
#include <app/layer_stack.hpp>
#include <asset_manager/asset_manager.hpp>
#include <input/events/event.hpp>
#include <input/events/keyboard.hpp>
#include <input/events/window.hpp>
#include <input/input.hpp>
#include <lt_debug/assertions.hpp>
#include <ranges>
#include <renderer/blender.hpp>
#include <renderer/graphics_context.hpp>
#include <renderer/render_command.hpp>
#include <renderer/renderer.hpp>
#include <ui/ui.hpp>
#include <window/window.hpp>
#include <app/system.hpp>
namespace lt {
Application *Application::s_instance = nullptr;
Application::Application(): m_window(nullptr)
{
ensure(!s_instance, "Application constructed twice");
s_instance = this;
m_window = Window::create([this](auto &&PH1) { on_event(std::forward<decltype(PH1)>(PH1)); });
// create graphics context
m_graphics_context = GraphicsContext::create(
GraphicsAPI::OpenGL,
(GLFWwindow *)m_window->get_handle()
);
AssetManager::load_shader(
"LT_ENGINE_RESOURCES_TEXTURE_SHADER",
"data/assets/shaders/texture/vs.asset",
"data/assets/shaders/texture/ps.asset"
);
AssetManager::load_shader(
"LT_ENGINE_RESOURCES_TINTED_TEXTURE_SHADER",
"data/assets/shaders/tinted_texture/vs.asset",
"data/assets/shaders/tinted_texture/ps.asset"
);
AssetManager::load_shader(
"LT_ENGINE_RESOURCES_QUAD_SHADER",
"data/assets/shaders/quads/vs.asset",
"data/assets/shaders/quads/ps.asset"
);
m_renderer = Renderer::create(
(GLFWwindow *)m_window->get_handle(),
lt::GraphicsContext::get_shared_context(),
Renderer::CreateInfo {
.quad_renderer_shader = AssetManager::get_shader("LT_ENGINE_RESOURCES_QUAD_SHADER"),
.texture_renderer_shader = AssetManager::get_shader(
"LT_ENGINE_RESOURCES_TEXTURE_SHADER"
),
.tinted_texture_renderer_shader = AssetManager::get_shader(
"LT_ENGINE_RESOURCES_TINTED_"
"TEXTURE_SHADER"
),
}
);
ensure(m_graphics_context, "lWindow::lWindow: failed to create 'GraphicsContext'");
m_user_interface = UserInterface::create(
(GLFWwindow *)m_window->get_handle(),
lt::GraphicsContext::get_shared_context()
);
m_layer_stack = create_scope<LayerStack>();
}
Application::~Application()
{
/** This is required to make forward-declarations possible:
* https://stackoverflow.com/questions/34072862/why-is-error-invalid-application-of-sizeof-to-an-incomplete-type-using-uniqu
*/
}
namespace lt::app {
void Application::game_loop()
{
m_window->set_visibility(true);
while (!m_window->is_closed())
while (true)
{
update_layers();
render_layers();
render_user_interface();
poll_events();
}
}
void Application::quit()
{
s_instance->m_window->close();
}
void Application::update_layers()
{
for (auto &it : *m_layer_stack)
{
// narrowing double -> float
it->on_update(static_cast<float>(m_timer.elapsed_time().count()));
}
// TODO(Light): each layer should have their own "delta time"
m_timer.reset();
}
void Application::render_layers()
{
m_renderer->begin_frame();
for (auto &it : *m_layer_stack)
{
it->on_render();
}
m_renderer->end_frame();
}
void Application::render_user_interface()
{
m_user_interface->begin();
for (auto &it : *m_layer_stack)
{
it->on_user_interface_update();
}
m_user_interface->end();
}
void Application::poll_events()
{
m_window->poll_events();
}
void Application::on_event(const Event &event)
{
// window
if (event.has_category(WindowEventCategory))
{
m_window->on_event(event);
if (event.get_event_type() == EventType::WindowResized)
for (auto &system : m_systems)
{
m_renderer->on_window_resize(dynamic_cast<const WindowResizedEvent &>(event));
if (system->tick())
{
return;
}
}
}
// input
if (event.has_category(InputEventCategory))
{
Input::instance().on_event(event);
if (!Input::instance().is_receiving_game_events())
for (auto &system : m_systems_to_be_registered)
{
return;
m_systems.emplace_back(system)->on_register();
}
}
for (auto &it : std::ranges::reverse_view(*m_layer_stack))
{
if (it->on_event(event))
for (auto &system : m_systems_to_be_unregistered)
{
m_systems.erase(
std::remove(m_systems.begin(), m_systems.end(), system),
m_systems.end()
);
}
if (m_systems.empty())
{
return;
}
}
}
[[nodiscard]] auto Application::sanity_check() const -> bool
void Application::register_system(Ref<app::ISystem> system)
{
log_inf("Checking application sanity...");
ensure(s_instance, "Application not constructed!?");
ensure(m_window, "Window is not initialized");
ensure(m_user_interface, "User interface is not initialized");
ensure(m_graphics_context, "Graphics context is not initialized");
ensure(m_renderer, "Renderer is not initialized");
ensure(m_layer_stack, "Layer_stack is not initialized");
ensure(!m_layer_stack->is_empty(), "Layer_stack is empty");
log_inf("Logging application state...");
this->log_debug_data();
m_graphics_context->log_debug_data();
m_user_interface->log_debug_data();
return true;
m_systems.emplace_back(std::move(system));
}
void Application::log_debug_data() const
void Application::unregister_system(Ref<app::ISystem> system)
{
log_inf("Platform::");
log_inf(" Platform name: {}", constants::platform_name);
log_inf(" Platform identifier: {}", std::to_underlying(constants::platform));
log_inf(" CWD: {}", std::filesystem::current_path().generic_string());
m_systems_to_be_unregistered.emplace_back(std::move(system));
}
} // namespace lt
} // namespace lt::app

View file

@ -1,47 +0,0 @@
#include <app/layer.hpp>
#include <input/events/char.hpp>
#include <input/events/event.hpp>
#include <input/events/keyboard.hpp>
#include <input/events/mouse.hpp>
#include <input/events/window.hpp>
namespace lt {
Layer::Layer(std::string name): m_layer_name(std::move(name))
{
}
auto Layer::on_event(const Event &event) -> bool
{
switch (event.get_event_type())
{
case EventType::MouseMoved: return on_mouse_moved(dynamic_cast<const MouseMovedEvent &>(event));
case EventType::ButtonPressed:
return on_button_pressed(dynamic_cast<const ButtonPressedEvent &>(event));
case EventType::ButtonReleased:
return on_button_released(dynamic_cast<const ButtonReleasedEvent &>(event));
case EventType::WheelScrolled:
return on_wheel_scrolled(dynamic_cast<const WheelScrolledEvent &>(event));
case EventType::KeyPressed: return on_key_pressed(dynamic_cast<const KeyPressedEvent &>(event));
case EventType::KeyRepeated: return on_key_repeat(dynamic_cast<const KeyRepeatEvent &>(event));
case EventType::KeyReleased:
return on_key_released(dynamic_cast<const KeyReleasedEvent &>(event));
case EventType::SetChar: return on_set_char(dynamic_cast<const SetCharEvent &>(event));
case EventType::WindowClosed:
return on_window_closed(dynamic_cast<const WindowClosedEvent &>(event));
case EventType::WindowResized:
return on_window_resized(dynamic_cast<const WindowResizedEvent &>(event));
case EventType::WindowMoved:
return on_window_moved(dynamic_cast<const WindowMovedEvent &>(event));
case EventType::WindowLostFocus:
return on_window_lost_focus(dynamic_cast<const WindowLostFocusEvent &>(event));
case EventType::WindowGainFocus:
return on_window_gain_focus(dynamic_cast<const WindowGainFocusEvent &>(event));
default: ensure(false, "Invalid event: {}", event.get_info_lt_log());
}
}
} // namespace lt

View file

@ -1,18 +0,0 @@
#include <app/layer.hpp>
#include <app/layer_stack.hpp>
namespace lt {
void LayerStack::attach_layer(Ref<Layer> layer)
{
log_trc("Attaching layer [{}]", layer->get_name());
m_layers.emplace_back(std::move(layer));
}
void LayerStack::detach_layer(const Ref<Layer> &layer)
{
log_trc("Detaching layer [{}]", layer->get_name());
m_layers.erase(std::find(m_layers.begin(), m_layers.end(), layer));
}
} // namespace lt

View file

@ -1,18 +1,15 @@
#pragma once
#include <time/timer.hpp>
namespace lt::app {
namespace lt {
class Renderer;
class Window;
class Event;
class GraphicsContext;
class UserInterface;
class LayerStack;
class ISystem;
extern Scope<class Application> create_application();
/** The main application class.
* Think of this like an aggregate of systems, you register systems through this interface.
* Then they'll tick every "application frame".
*/
class Application
{
public:
@ -24,54 +21,24 @@ public:
auto operator=(Application &&) -> Application & = delete;
virtual ~Application();
[[nodiscard]] auto sanity_check() const -> bool;
virtual ~Application() = default;
void game_loop();
[[nodiscard]] auto get_window() -> Window &
{
return *m_window;
}
void register_system(Ref<app::ISystem> system);
[[nodiscard]] auto get_layer_stack() -> LayerStack &
{
return *m_layer_stack;
}
static void quit();
void unregister_system(Ref<app::ISystem> system);
protected:
Application();
Application() = default;
private:
void update_layers();
std::vector<Ref<app::ISystem>> m_systems;
void render_layers();
std::vector<Ref<app::ISystem>> m_systems_to_be_unregistered;
void render_user_interface();
void poll_events();
void on_event(const Event &event);
void log_debug_data() const;
Timer m_timer;
Scope<Window> m_window;
Scope<UserInterface> m_user_interface;
Scope<GraphicsContext> m_graphics_context;
Scope<Renderer> m_renderer;
Scope<LayerStack> m_layer_stack;
static Application *s_instance;
std::vector<Ref<app::ISystem>> m_systems_to_be_registered;
};
} // namespace lt
} // namespace lt::app

View file

@ -2,21 +2,21 @@
#include <app/application.hpp>
int main(int argc, char *argv[]) // NOLINT
auto main(int argc, char *argv[]) -> int32_t
try
{
std::ignore = argc;
std::ignore = argv;
auto application = lt::Scope<lt::Application> {};
auto application = lt::Scope<lt::app::Application> {};
application = lt::create_application();
lt::ensure(application, "Failed to create application");
lt::ensure(application->sanity_check(), "Failed to verify the sanity of the application");
application = lt::app::create_application();
if (!application)
{
throw std::runtime_error { "Failed to create application\n" };
}
application->game_loop();
return EXIT_SUCCESS;
}
catch (const std::exception &exp)

View file

@ -1,116 +0,0 @@
#pragma once
namespace lt {
class Event;
class MouseMovedEvent;
class ButtonPressedEvent;
class ButtonReleasedEvent;
class WheelScrolledEvent;
class KeyPressedEvent;
class KeyRepeatEvent;
class KeyReleasedEvent;
class SetCharEvent;
class WindowClosedEvent;
class WindowResizedEvent;
class WindowMovedEvent;
class WindowLostFocusEvent;
class WindowGainFocusEvent;
class Layer
{
public:
Layer(std::string name);
virtual ~Layer() = default;
[[nodiscard]] auto get_name() const -> const std::string &
{
return m_layer_name;
}
virtual void on_update(float deltaTime)
{
}
virtual void on_user_interface_update()
{
}
virtual void on_render()
{
}
auto on_event(const Event &event) -> bool;
protected:
std::string m_layer_name;
virtual auto on_mouse_moved(const MouseMovedEvent & /*event*/) -> bool
{
return false;
}
virtual auto on_button_pressed(const ButtonPressedEvent & /*event*/) -> bool
{
return false;
}
virtual auto on_button_released(const ButtonReleasedEvent & /*event*/) -> bool
{
return false;
}
virtual auto on_wheel_scrolled(const WheelScrolledEvent & /*event*/) -> bool
{
return false;
}
virtual auto on_key_pressed(const KeyPressedEvent & /*event*/) -> bool
{
return false;
}
virtual auto on_key_repeat(const KeyRepeatEvent & /*event*/) -> bool
{
return false;
}
virtual auto on_key_released(const KeyReleasedEvent & /*event*/) -> bool
{
return false;
}
virtual auto on_set_char(const SetCharEvent & /*event*/) -> bool
{
return false;
}
virtual auto on_window_closed(const WindowClosedEvent & /*event*/) -> bool
{
return false;
}
virtual auto on_window_resized(const WindowResizedEvent & /*event*/) -> bool
{
return false;
}
virtual auto on_window_moved(const WindowMovedEvent & /*event*/) -> bool
{
return false;
}
virtual auto on_window_lost_focus(const WindowLostFocusEvent & /*event*/) -> bool
{
return false;
}
virtual auto on_window_gain_focus(const WindowGainFocusEvent & /*event*/) -> bool
{
return false;
}
};
} // namespace lt

View file

@ -1,50 +0,0 @@
#pragma once
namespace lt {
class Layer;
class Event;
class LayerStack
{
public:
template<typename Layer_T, typename... Args>
void emplace_layer(Args &&...args)
{
attach_layer(create_ref<Layer_T>(std::forward<Args>(args)...));
}
void attach_layer(Ref<Layer> layer);
void detach_layer(const Ref<Layer> &layer);
[[nodiscard]] auto is_empty() const -> bool
{
return m_layers.empty();
}
[[nodiscard]] auto begin() -> std::vector<Ref<Layer>>::iterator
{
return m_layers.begin();
}
[[nodiscard]] auto end() -> std::vector<Ref<Layer>>::iterator
{
return m_layers.end();
}
[[nodiscard]] auto rbegin() -> std::vector<Ref<Layer>>::reverse_iterator
{
return m_layers.rbegin();
}
[[nodiscard]] auto rend() -> std::vector<Ref<Layer>>::reverse_iterator
{
return m_layers.rend();
}
private:
std::vector<Ref<Layer>> m_layers;
};
} // namespace lt

View file

@ -0,0 +1,27 @@
#pragma once
namespace lt::app {
class ISystem
{
public:
ISystem() = default;
virtual ~ISystem() = default;
ISystem(ISystem &&) = default;
ISystem(const ISystem &) = delete;
auto operator=(ISystem &&) -> ISystem & = default;
auto operator=(const ISystem &) -> ISystem & = delete;
virtual void on_register() = 0;
virtual void on_unregister() = 0;
virtual auto tick() -> bool = 0;
};
} // namespace lt::app

View file

@ -5,5 +5,5 @@ add_library_module(asset_manager
target_link_libraries(
asset_manager
PUBLIC asset_parser
PRIVATE renderer
PRIVATE logger
)

View file

@ -1,36 +1,36 @@
#pragma once
#include <format>
#include <logger/logger.hpp>
#include <source_location>
namespace lt {
struct FailedAssertion: std::exception
template<typename Expression_T, typename... Args_T>
struct ensure
{
FailedAssertion(const char *file, int line)
ensure(
Expression_T expression,
std::format_string<Args_T...> fmt,
Args_T &&...args,
const std::source_location &location = std::source_location::current()
)
{
log_crt("Assertion failed in: {} (line {})", file, line);
if (!static_cast<bool>(expression))
{
throw std::runtime_error { std::format(
"exception: {}\nlocation: {}:{}",
std::format(fmt, std::forward<Args_T>(args)...),
location.file_name(),
location.line()
) };
}
}
};
template<typename Expression_T, typename... Args>
constexpr void ensure(Expression_T &&expression, std::format_string<Args...> fmt, Args &&...args)
{
if (!static_cast<bool>(expression))
{
Logger::log(LogLvl::critical, fmt, std::forward<Args>(args)...);
throw ::lt::FailedAssertion(__FILE__, __LINE__);
}
}
template<typename Expression_T>
constexpr void ensure(Expression_T &&expression, const char *message)
{
if (!static_cast<bool>(expression))
{
Logger::log(LogLvl::critical, message);
throw ::lt::FailedAssertion(__FILE__, __LINE__);
}
}
template<typename Expression_T, typename... Args_T>
ensure(Expression_T, std::format_string<Args_T...>, Args_T &&...)
-> ensure<Expression_T, Args_T...>;
} // namespace lt

View file

@ -1,5 +1,4 @@
add_library_module(ecs entity.cpp scene.cpp uuid.cpp serializer.cpp)
add_library_module(ecs entity.cpp scene.cpp uuid.cpp )
target_link_libraries(ecs
PUBLIC logger lt_debug EnTT::EnTT renderer input camera
PRIVATE yaml-cpp::yaml-cpp asset_manager
PUBLIC logger lt_debug EnTT::EnTT input camera math
)

View file

@ -1,73 +1,9 @@
#include <camera/component.hpp>
#include <ecs/components.hpp>
#include <ecs/entity.hpp>
#include <ecs/scene.hpp>
#include <renderer/renderer.hpp>
namespace lt {
void Scene::on_create()
{
/* native scripts */
{
m_registry.view<NativeScriptComponent>().each([](NativeScriptComponent &nsc) {
if (nsc.instance == nullptr)
{
nsc.instance = nsc.CreateInstance();
nsc.instance->on_create();
}
});
}
}
void Scene::on_update(float deltaTime)
{
/* native scripts */
{
m_registry.view<NativeScriptComponent>().each([=](NativeScriptComponent &nsc) {
nsc.instance->on_update(deltaTime);
});
}
}
void Scene::on_render(const Ref<Framebuffer> &targetFrameBuffer /* = nullptr */)
{
auto *sceneCamera = (Camera *)nullptr;
auto *sceneCameraTransform = (TransformComponent *)nullptr;
/* scene camera */
{
m_registry.group(entt::get<TransformComponent, CameraComponent>)
.each([&](TransformComponent &transformComp, CameraComponent &cameraComp) {
if (cameraComp.isPrimary)
{
sceneCamera = &cameraComp.camera;
sceneCameraTransform = &transformComp;
}
});
}
/* draw quads */
{
if (sceneCamera)
{
Renderer::begin_scene(sceneCamera, *sceneCameraTransform, targetFrameBuffer);
m_registry.group(entt::get<TransformComponent, SpriteRendererComponent>)
.each([](TransformComponent &transformComp,
SpriteRendererComponent &spriteRendererComp) {
Renderer::draw_quad(
transformComp,
spriteRendererComp.tint,
spriteRendererComp.texture
);
});
Renderer::end_scene();
}
}
}
auto Scene::create_entity(const std::string &name, const TransformComponent &transform) -> Entity
{
return create_entity_with_uuid(name, UUID(), transform);
@ -75,21 +11,6 @@ auto Scene::create_entity(const std::string &name, const TransformComponent &tra
auto Scene::get_entity_by_tag(const std::string &tag) -> Entity
{
// TagComponent tagComp(tag);
// entt::entity entity = entt::to_entity(m_registry, tagComp);
auto entity = Entity {};
m_registry.view<TagComponent>().each([&](TagComponent &tagComp) {
// if (tagComp.tag == tag)
// entity = entity(entt::to_entity(m_registry, tagComp), this);
});
if (entity.is_valid())
{
return entity;
}
ensure(false, "Scene::get_entity_by_tag: failed to find entity by tag: {}", tag);
return {};
}

View file

@ -3,6 +3,7 @@
#include <ecs/components/transform.hpp>
#include <ecs/uuid.hpp>
#include <entt/entt.hpp>
#include <functional>
namespace lt {
@ -12,11 +13,17 @@ class Framebuffer;
class Scene
{
public:
void on_create();
template<typename... T>
auto group()
{
return m_registry.group(entt::get<T...>);
}
void on_update(float deltaTime);
void on_render(const Ref<Framebuffer> &targetFrameBuffer = nullptr);
template<typename T>
auto view()
{
return m_registry.view<T>();
}
auto create_entity(
const std::string &name,
@ -25,6 +32,12 @@ public:
auto get_entity_by_tag(const std::string &tag) -> Entity;
auto get_entt_registry() -> entt::registry &
{
return m_registry;
}
private:
friend class Entity;
@ -41,4 +54,12 @@ private:
) -> Entity;
};
namespace ecs {
using Registry = Scene;
using Entity = ::lt::Entity;
} // namespace ecs
} // namespace lt

View file

@ -1,2 +1,2 @@
add_library_module(input input.cpp)
target_link_libraries(input PUBLIC math imgui::imgui logger)
target_link_libraries(input PUBLIC surface math imgui::imgui logger)

View file

@ -1,10 +1,5 @@
#include <imgui.h>
#include <input/events/char.hpp>
#include <input/events/event.hpp>
#include <input/events/keyboard.hpp>
#include <input/events/mouse.hpp>
#include <input/input.hpp>
#include <input/key_codes.hpp>
#include <logger/logger.hpp>
namespace lt {

View file

@ -0,0 +1,5 @@
#include <input/system.hpp>
namespace lt::input {
} // namespace lt::input

View file

@ -1,41 +0,0 @@
#pragma once
#include <input/events/event.hpp>
#include <sstream>
namespace lt {
class SetCharEvent: public Event
{
public:
SetCharEvent(unsigned int character): m_character(character)
{
}
[[nodiscard]] auto get_character() const -> int
{
return m_character;
}
[[nodiscard]] auto get_info_lt_log() const -> std::string override
{
std::stringstream ss;
ss << "CharSet: " << m_character;
return ss.str();
}
[[nodiscard]] auto get_event_type() const -> EventType override
{
return ::lt::EventType::SetChar;
}
[[nodiscard]] auto has_category(EventCategory category) const -> bool override
{
return static_cast<uint8_t>(InputEventCategory | KeyboardEventCategory) & category;
}
private:
const unsigned int m_character;
};
} // namespace lt

View file

@ -1,56 +0,0 @@
#pragma once
namespace lt {
enum class EventType : uint8_t
{
None = 0,
// input
MouseMoved,
WheelScrolled,
ButtonPressed,
ButtonReleased,
KeyPressed,
KeyRepeated,
KeyReleased,
SetChar,
// window
WindowMoved,
WindowResized,
WindowClosed,
WindowLostFocus,
WindowGainFocus,
};
enum EventCategory : uint8_t
{
None = 0,
WindowEventCategory = bit(0),
InputEventCategory = bit(1),
KeyboardEventCategory = bit(2),
MouseEventCategory = bit(3),
};
class Event
{
public:
Event() = default;
virtual ~Event() = default;
[[nodiscard]] virtual auto get_event_type() const -> EventType = 0;
[[nodiscard]] virtual auto get_info_lt_log() const -> std::string = 0;
[[nodiscard]] virtual auto has_category(EventCategory category) const -> bool = 0;
friend auto operator<<(std::ostream &os, const Event &e) -> std::ostream &
{
return os << e.get_info_lt_log();
}
};
} // namespace lt

View file

@ -1,107 +0,0 @@
#pragma once
#include <input/events/event.hpp>
#include <sstream>
namespace lt {
class KeyPressedEvent: public Event
{
public:
KeyPressedEvent(int key): m_key(key)
{
}
[[nodiscard]] auto get_key() const -> int
{
return m_key;
}
[[nodiscard]] auto get_info_lt_log() const -> std::string override
{
std::stringstream ss;
ss << "KeyPressed: " << m_key;
return ss.str();
}
[[nodiscard]] auto get_event_type() const -> EventType override
{
return ::lt::EventType::KeyPressed;
}
[[nodiscard]] auto has_category(EventCategory category) const -> bool override
{
return static_cast<uint8_t>(InputEventCategory | KeyboardEventCategory) & category;
}
private:
const int m_key;
};
class KeyRepeatEvent: public Event
{
public:
KeyRepeatEvent(int key): m_key(key)
{
}
[[nodiscard]] auto get_key() const -> int
{
return m_key;
}
[[nodiscard]] auto get_info_lt_log() const -> std::string override
{
std::stringstream ss;
ss << "KeyRepeated: " << m_key;
return ss.str();
}
[[nodiscard]] auto get_event_type() const -> EventType override
{
return ::lt::EventType::KeyRepeated;
}
[[nodiscard]] auto has_category(EventCategory category) const -> bool override
{
return static_cast<uint8_t>(InputEventCategory | KeyboardEventCategory) & category;
}
private:
const int m_key;
};
class KeyReleasedEvent: public Event
{
public:
KeyReleasedEvent(int key): m_key(key)
{
}
[[nodiscard]] auto get_key() const -> int
{
return m_key;
}
[[nodiscard]] auto get_info_lt_log() const -> std::string override
{
std::stringstream ss;
ss << "KeyReleased: " << m_key;
return ss.str();
}
[[nodiscard]] auto get_event_type() const -> EventType override
{
return ::lt::EventType::KeyReleased;
}
[[nodiscard]] auto has_category(EventCategory category) const -> bool override
{
return static_cast<uint8_t>(InputEventCategory | KeyboardEventCategory) & category;
}
private:
const int m_key;
};
} // namespace lt

View file

@ -1,151 +0,0 @@
#pragma once
#include <input/events/event.hpp>
#include <math/vec2.hpp>
#include <sstream>
namespace lt {
class MouseMovedEvent: public Event
{
public:
MouseMovedEvent(float x, float y): m_position(x, y)
{
}
[[nodiscard]] auto get_position() const -> const math::vec2 &
{
return m_position;
}
[[nodiscard]] auto get_x() const -> float
{
return m_position.x;
}
[[nodiscard]] auto get_y() const -> float
{
return m_position.y;
}
[[nodiscard]] auto get_info_lt_log() const -> std::string override
{
std::stringstream ss;
ss << "MouseMoved: " << m_position.x << ", " << m_position.y;
return ss.str();
}
[[nodiscard]] auto get_event_type() const -> EventType override
{
return ::lt::EventType::MouseMoved;
}
[[nodiscard]] auto has_category(EventCategory category) const -> bool override
{
return static_cast<uint8_t>(InputEventCategory | MouseEventCategory) & category;
}
private:
const math::vec2 m_position;
};
class WheelScrolledEvent: public Event
{
public:
WheelScrolledEvent(float offset): m_offset(offset)
{
}
[[nodiscard]] auto get_offset() const -> float
{
return m_offset;
}
[[nodiscard]] auto get_info_lt_log() const -> std::string override
{
std::stringstream ss;
ss << "WheelScrolled: " << m_offset;
return ss.str();
}
[[nodiscard]] auto get_event_type() const -> EventType override
{
return ::lt::EventType::WheelScrolled;
}
[[nodiscard]] auto has_category(EventCategory category) const -> bool override
{
return static_cast<uint8_t>(InputEventCategory | MouseEventCategory) & category;
}
private:
const float m_offset;
};
class ButtonPressedEvent: public Event
{
public:
ButtonPressedEvent(int button): m_button(button)
{
}
[[nodiscard]] auto get_button() const -> int
{
return m_button;
}
[[nodiscard]] auto get_info_lt_log() const -> std::string override
{
std::stringstream ss;
ss << "ButtonPressed: " << m_button;
return ss.str();
}
[[nodiscard]] auto get_event_type() const -> EventType override
{
return ::lt::EventType::ButtonPressed;
}
[[nodiscard]] auto has_category(EventCategory category) const -> bool override
{
return static_cast<uint8_t>(InputEventCategory | MouseEventCategory) & category;
}
private:
const int m_button;
};
class ButtonReleasedEvent: public Event
{
public:
ButtonReleasedEvent(int button): m_button(button)
{
}
[[nodiscard]] auto get_button() const -> int
{
return m_button;
}
[[nodiscard]] auto get_info_lt_log() const -> std::string override
{
std::stringstream ss;
ss << "ButtonReleased: " << m_button;
return ss.str();
}
[[nodiscard]] auto get_event_type() const -> EventType override
{
return ::lt::EventType::ButtonReleased;
}
[[nodiscard]] auto has_category(EventCategory category) const -> bool override
{
return static_cast<uint8_t>(InputEventCategory | MouseEventCategory) & category;
}
private:
const int m_button;
};
} // namespace lt

View file

@ -1,133 +0,0 @@
#pragma once
#include <input/events/event.hpp>
#include <math/vec2.hpp>
#include <sstream>
namespace lt {
class WindowClosedEvent: public Event
{
public:
[[nodiscard]] auto get_info_lt_log() const -> std::string override
{
return "WindowClosedEvent";
}
[[nodiscard]] auto get_event_type() const -> EventType override
{
return ::lt::EventType::WindowClosed;
}
[[nodiscard]] auto has_category(EventCategory category) const -> bool override
{
return static_cast<uint8_t>(WindowEventCategory) & category;
}
};
class WindowMovedEvent: public Event
{
public:
WindowMovedEvent(int x, int y): m_position(x, y)
{
}
[[nodiscard]] auto get_position() const -> const math::ivec2 &
{
return m_position;
}
[[nodiscard]] auto get_info_lt_log() const -> std::string override
{
std::stringstream ss;
ss << "WindwoMoved: " << m_position.x << ", " << m_position.y;
return ss.str();
;
}
[[nodiscard]] auto get_event_type() const -> EventType override
{
return ::lt::EventType::WindowMoved;
}
[[nodiscard]] auto has_category(EventCategory category) const -> bool override
{
return static_cast<uint8_t>(WindowEventCategory) & category;
}
private:
const math::ivec2 m_position;
};
class WindowResizedEvent: public Event
{
public:
WindowResizedEvent(unsigned int width, unsigned int height): m_size(width, height)
{
}
[[nodiscard]] auto get_size() const -> const math::uvec2 &
{
return m_size;
}
[[nodiscard]] auto get_info_lt_log() const -> std::string override
{
std::stringstream ss;
ss << "WindowResized: " << m_size.x << ", " << m_size.y;
return ss.str();
}
[[nodiscard]] auto get_event_type() const -> EventType override
{
return ::lt::EventType::WindowResized;
}
[[nodiscard]] auto has_category(EventCategory category) const -> bool override
{
return static_cast<uint8_t>(WindowEventCategory) & category;
}
private:
const math::uvec2 m_size;
};
class WindowLostFocusEvent: public Event
{
public:
[[nodiscard]] auto get_info_lt_log() const -> std::string override
{
return "WindowLostFocus";
}
[[nodiscard]] auto get_event_type() const -> EventType override
{
return ::lt::EventType::WindowLostFocus;
}
[[nodiscard]] auto has_category(EventCategory category) const -> bool override
{
return static_cast<uint8_t>(WindowEventCategory) & category;
}
};
class WindowGainFocusEvent: public Event
{
public:
[[nodiscard]] auto get_info_lt_log() const -> std::string override
{
return "WindowGainFocus";
}
[[nodiscard]] auto get_event_type() const -> EventType override
{
return ::lt::EventType::WindowGainFocus;
}
[[nodiscard]] auto has_category(EventCategory category) const -> bool override
{
return static_cast<uint8_t>(WindowEventCategory) & category;
}
};
} // namespace lt

View file

@ -0,0 +1,64 @@
#pragma once
#include <surface/system.hpp>
namespace lt::input {
template<class... Ts>
struct overloads: Ts...
{
using Ts::operator()...;
};
/**
*
* @note If this system is attached, it will always consume the input events f rom surface.
* Therefore if you want any input detection mechanism, callbacks should be setup with this
* system and not directly with surface.
*/
class System
{
public:
System(lt::surface::System &surface_system)
{
surface_system.add_event_listener([this](auto &&event) {
return handle_event(std::forward<decltype(event)>(event));
});
};
private:
auto handle_event(const lt::surface::System::Event &event) -> bool
{
const auto visitor = overloads {
[this](const lt::surface::KeyPressedEvent &event) {
m_keys[event.get_key()] = true;
return true;
},
[](const lt::surface::KeyRepeatEvent &) { return false; },
[](const lt::surface::KeyReleasedEvent &) { return false; },
[](const lt::surface::KeySetCharEvent &) { return false; },
[](const lt::surface::MouseMovedEvent &) { return false; },
[](const lt::surface::WheelScrolledEvent &) { return false; },
[](const lt::surface::ButtonPressedEvent &) { return false; },
[](const lt::surface::ButtonReleasedEvent &) { return false; },
[](const auto &) { return false; },
};
return std::visit(visitor, event);
}
void setup_callbacks(GLFWwindow *handle);
std::array<bool, 512> m_keys {};
};
} // namespace lt::input

View file

@ -4,7 +4,6 @@
namespace lt::math {
/**
* let...
* a = h / w ==> for aspect ratio adjustment

View file

@ -1,16 +1,11 @@
add_library_module(libmirror
layers/editor_layer.cpp
panels/asset_browser.cpp
panels/properties.cpp
panels/scene_hierarchy.cpp
)
target_link_libraries(
libmirror
PUBLIC app
PUBLIC opengl::opengl
PUBLIC ui
PUBLIC imgui
PUBLIC input
INTERFACE
app
opengl::opengl
surface
)
add_test_module(libmirror

View file

@ -1,30 +1,77 @@
#include <app/application.hpp>
#include <app/entrypoint.hpp>
#include <app/layer_stack.hpp>
#include <app/system.hpp>
#include <math/vec2.hpp>
#include <mirror/layers/editor_layer.hpp>
#include <window/window.hpp>
#include <surface/system.hpp>
namespace lt {
class Mirror: public Application
template<class... Ts>
struct overloads: Ts...
{
using Ts::operator()...;
};
class Mirror: public app::Application
{
public:
Mirror()
{
get_window().set_properties(
Window::Properties {
.title = "Mirror",
.size = math::uvec2(1280u, 720u),
.vsync = true,
m_editor_registry = create_ref<ecs::Registry>();
setup_window_system();
register_systems();
m_window_system->add_event_listener(
m_window,
[&](const surface::SurfaceComponent::Event &event) {
const auto visitor = overloads {
[&](const lt::surface::KeyPressedEvent &event) {
std::cout << "key pressed: " << event.to_string() << std::endl;
if (event.get_key() == 81)
{
unregister_system(m_window_system);
log_inf("Quitting...");
}
return true;
},
[](const auto &) { return false; },
};
return std::visit(visitor, event);
}
);
get_layer_stack().emplace_layer<EditorLayer>("MirrorLayer");
}
void setup_window_system()
{
using lt::surface::SurfaceComponent;
m_window_system = create_ref<lt::surface::System>(m_editor_registry);
m_window = m_editor_registry->create_entity("Editor Window");
m_window.add_component<SurfaceComponent>(SurfaceComponent::CreateInfo {
.title = "Editor Window",
.resolution = { 800u, 600u },
.vsync = true,
.visible = true,
});
}
void register_systems()
{
register_system(m_window_system);
}
private:
Ref<ecs::Registry> m_editor_registry;
Ref<lt::surface::System> m_window_system;
lt::ecs::Entity m_window;
};
auto create_application() -> Scope<Application>
auto app::create_application() -> Scope<app::Application>
{
return create_scope<Mirror>();
}

View file

@ -82,14 +82,15 @@ void AssetBrowserPanel::on_user_interface_update()
}
// Button
const auto path_str = path.string();
ImGui::TableNextColumn();
ImGui::PushID(path.c_str());
ImGui::PushID(path_str.c_str());
switch (asset_type)
{
// Directory
case AssetType::directory:
if (ImGui::ImageButton(
path.c_str(),
path_str.c_str(),
m_directory_texture->get_texture(),
ImVec2(m_file_size, m_file_size)
))
@ -101,7 +102,7 @@ void AssetBrowserPanel::on_user_interface_update()
// Scene
case AssetType::scene:
if (ImGui::ImageButton(
path.c_str(),
path_str.c_str(),
m_scene_texture->get_texture(),
ImVec2(m_file_size, m_file_size)
))
@ -115,7 +116,7 @@ void AssetBrowserPanel::on_user_interface_update()
// Image
case AssetType::image:
if (ImGui::ImageButton(
path.c_str(),
path_str.c_str(),
m_image_texture->get_texture(),
ImVec2(m_file_size, m_file_size)
))
@ -126,7 +127,7 @@ void AssetBrowserPanel::on_user_interface_update()
// Text
case AssetType::text:
if (ImGui::ImageButton(
path.c_str(),
path_str.c_str(),
m_text_texture->get_texture(),
ImVec2(m_file_size, m_file_size)
))

View file

@ -1,4 +1,5 @@
add_library_module(renderer
system.cpp
blender.cpp
buffers.cpp
framebuffer.cpp
@ -19,6 +20,7 @@ add_library_module(renderer
gl/shader.cpp
gl/texture.cpp
gl/vertex_layout.cpp
vk/instance.cpp
)
target_link_libraries(
@ -34,4 +36,15 @@ target_link_libraries(
PUBLIC yaml-cpp::yaml-cpp
PUBLIC EnTT::EnTT
PRIVATE lt_debug
PRIVATE window
PUBLIC vulkan
)
add_test_module(renderer
system.test.cpp
)
target_link_libraries(
renderer_tests
PRIVATE lt_debug
PRIVATE window
)

View file

@ -4,12 +4,13 @@
#include <renderer/blender.hpp>
#include <renderer/buffers.hpp>
#include <renderer/framebuffer.hpp>
#include <renderer/programs/quad.hpp>
#include <renderer/programs/texture.hpp>
#include <renderer/programs/tinted_texture.hpp>
#include <renderer/render_command.hpp>
#include <renderer/renderer.hpp>
#include <renderer/shader.hpp>
#include <renderer/texture.hpp>
#include <span>
#include <utility>
namespace lt {
@ -21,19 +22,25 @@ Renderer::Renderer(
CreateInfo create_info
)
: m_quad_renderer(
LT_MAX_QUAD_RENDERER_VERTICES,
shared_context,
std::move(create_info.quad_renderer_shader)
create_scope<QuadRendererProgram>(
LT_MAX_QUAD_RENDERER_VERTICES,
shared_context,
std::move(create_info.quad_renderer_shader)
)
)
, m_texture_renderer(
LT_MAX_TEXTURE_RENDERER_VERTICES,
shared_context,
std::move(create_info.texture_renderer_shader)
create_scope<TextureRendererProgram>(
LT_MAX_TEXTURE_RENDERER_VERTICES,
shared_context,
std::move(create_info.texture_renderer_shader)
)
)
, m_tinted_texture_renderer(
LT_MAX_TINTED_TEXTURE_RENDERER_VERTICES,
shared_context,
std::move(create_info.tinted_texture_renderer_shader)
create_scope<TintedTextureRendererProgram>(
LT_MAX_TINTED_TEXTURE_RENDERER_VERTICES,
shared_context,
std::move(create_info.tinted_texture_renderer_shader)
)
)
, m_view_projection_buffer(nullptr)
, m_render_command(nullptr)
@ -55,6 +62,10 @@ Renderer::Renderer(
m_blender->enable(BlendFactor::SRC_ALPHA, BlendFactor::INVERSE_SRC_ALPHA);
}
Renderer::~Renderer() // NOLINT
{
}
auto Renderer::create(
GLFWwindow *windowHandle,
Ref<SharedContext> sharedContext,
@ -114,7 +125,7 @@ void Renderer::draw_quad_impl(
//==================== DRAW_QUAD_TINT ====================//
void Renderer::draw_quad_impl(const math::mat4 &transform, const math::vec4 &tint)
{
auto map = std::span<QuadRendererProgram::QuadVertexData> { m_quad_renderer.get_map_current(),
auto map = std::span<QuadRendererProgram::QuadVertexData> { m_quad_renderer->get_map_current(),
4 };
// top left
@ -134,7 +145,7 @@ void Renderer::draw_quad_impl(const math::mat4 &transform, const math::vec4 &tin
map[3].tint = tint;
// advance
if (!m_quad_renderer.advance())
if (!m_quad_renderer->advance())
{
log_wrn("Exceeded LT_MAX_QUAD_RENDERER_VERTICES: {}", LT_MAX_QUAD_RENDERER_VERTICES);
flush_scene();
@ -147,7 +158,7 @@ void Renderer::draw_quad_impl(const math::mat4 &transform, const Ref<Texture> &t
texture->bind();
auto map = std::span<TextureRendererProgram::TextureVertexData> {
m_texture_renderer.get_map_current(),
m_texture_renderer->get_map_current(),
4
};
@ -168,7 +179,7 @@ void Renderer::draw_quad_impl(const math::mat4 &transform, const Ref<Texture> &t
map[3].texcoord = { 0.0f, 1.0f };
// advance
if (!m_texture_renderer.advance())
if (!m_texture_renderer->advance())
{
log_wrn("Exceeded LT_MAX_TEXTURE_RENDERER_VERTICES: {}", LT_MAX_TEXTURE_RENDERER_VERTICES);
flush_scene();
@ -185,7 +196,7 @@ void Renderer::draw_quad_impl(
texture->bind();
auto map = std::span<TintedTextureRendererProgram::TintedTextureVertexData> {
m_tinted_texture_renderer.get_map_current(),
m_tinted_texture_renderer->get_map_current(),
4
};
@ -209,7 +220,7 @@ void Renderer::draw_quad_impl(
map[3].tint = tint;
map[3].texcoord = { 0.0f, 1.0f };
if (!m_tinted_texture_renderer.advance())
if (!m_tinted_texture_renderer->advance())
{
log_wrn("Exceeded LT_MAX_TEXTURE_RENDERER_VERTICES: {}", LT_MAX_TEXTURE_RENDERER_VERTICES);
flush_scene();
@ -256,66 +267,66 @@ void Renderer::begin_scene_impl(
m_view_projection_buffer->un_map();
// map renderers
m_quad_renderer.map();
m_texture_renderer.map();
m_tinted_texture_renderer.map();
m_quad_renderer->map();
m_texture_renderer->map();
m_tinted_texture_renderer->map();
}
void Renderer::flush_scene()
{
/* tinted texture renderer */
m_tinted_texture_renderer.un_map();
if (m_tinted_texture_renderer.get_quad_count())
m_tinted_texture_renderer->un_map();
if (m_tinted_texture_renderer->get_quad_count())
{
m_tinted_texture_renderer.bind();
m_render_command->draw_indexed(m_tinted_texture_renderer.get_quad_count() * 6u);
m_tinted_texture_renderer->bind();
m_render_command->draw_indexed(m_tinted_texture_renderer->get_quad_count() * 6u);
}
/* quad renderer */
m_quad_renderer.un_map();
if (m_quad_renderer.get_quad_count())
m_quad_renderer->un_map();
if (m_quad_renderer->get_quad_count())
{
m_quad_renderer.bind();
m_render_command->draw_indexed(m_quad_renderer.get_quad_count() * 6u);
m_quad_renderer->bind();
m_render_command->draw_indexed(m_quad_renderer->get_quad_count() * 6u);
}
/* texture renderer */
m_texture_renderer.un_map();
if (m_texture_renderer.get_quad_count())
m_texture_renderer->un_map();
if (m_texture_renderer->get_quad_count())
{
m_texture_renderer.bind();
m_render_command->draw_indexed(m_texture_renderer.get_quad_count() * 6u);
m_texture_renderer->bind();
m_render_command->draw_indexed(m_texture_renderer->get_quad_count() * 6u);
}
m_quad_renderer.map();
m_texture_renderer.map();
m_tinted_texture_renderer.map();
m_quad_renderer->map();
m_texture_renderer->map();
m_tinted_texture_renderer->map();
}
void Renderer::end_scene_impl()
{
/* tinted texture renderer */
m_tinted_texture_renderer.un_map();
if (m_tinted_texture_renderer.get_quad_count())
m_tinted_texture_renderer->un_map();
if (m_tinted_texture_renderer->get_quad_count())
{
m_tinted_texture_renderer.bind();
m_render_command->draw_indexed(m_tinted_texture_renderer.get_quad_count() * 6u);
m_tinted_texture_renderer->bind();
m_render_command->draw_indexed(m_tinted_texture_renderer->get_quad_count() * 6u);
}
/* quad renderer */
m_quad_renderer.un_map();
if (m_quad_renderer.get_quad_count())
m_quad_renderer->un_map();
if (m_quad_renderer->get_quad_count())
{
m_quad_renderer.bind();
m_render_command->draw_indexed(m_quad_renderer.get_quad_count() * 6u);
m_quad_renderer->bind();
m_render_command->draw_indexed(m_quad_renderer->get_quad_count() * 6u);
}
/* texture renderer */
m_texture_renderer.un_map();
if (m_texture_renderer.get_quad_count())
m_texture_renderer->un_map();
if (m_texture_renderer->get_quad_count())
{
m_texture_renderer.bind();
m_render_command->draw_indexed(m_texture_renderer.get_quad_count() * 6u);
m_texture_renderer->bind();
m_render_command->draw_indexed(m_texture_renderer->get_quad_count() * 6u);
}
// reset frame buffer

View file

@ -0,0 +1,18 @@
#include <lt_debug/assertions.hpp>
#include <renderer/system.hpp>
namespace lt::renderer {
System::System(InitRequirements requirements): m_registry(std::move(requirements.registry))
{
ensure(m_registry, "null registry");
ensure(requirements.glfw_window_handle, "null glfw handle");
}
System::~System() = default;
void System::tick(TickRequirements requirements)
{
}
} // namespace lt::renderer

View file

@ -0,0 +1,50 @@
#include <ranges>
#include <renderer/system.hpp>
#include <test/test.hpp>
#include <window/window.hpp>
using namespace lt;
using lt::test::Case;
using lt::test::Suite;
Suite raii = [] {
using lt::test::expect_true;
using lt::test::expect_throw;
using renderer::System;
auto *window = static_cast<GLFWwindow *>(lt::Window::create([](auto &&PH1) {})->get_handle());
Case { "happy" } = [=] {
std::ignore = System { {
.glfw_window_handle = window,
.registry = create_ref<ecs::Registry>(),
} };
};
Case { "unhappy" } = [=] {
expect_throw([=] {
std::ignore = System { {
.glfw_window_handle = window,
.registry = {},
} };
});
expect_throw([=] {
std::ignore = System { {
.glfw_window_handle = {},
.registry = create_ref<ecs::Registry>(),
} };
});
};
Case { "plenty" } = [=] {
for (auto idx : std::views::iota(0, 100'001))
{
std::ignore = System { {
.glfw_window_handle = window,
.registry = create_ref<ecs::Registry>(),
} };
}
};
};

View file

@ -0,0 +1,434 @@
#include <renderer/vk/backend.hpp>
#if defined(_WIN32)
#elif defined(__unix__)
#include <dlfcn.h>
namespace {
void *library; // NOLINT
}
#endif
namespace lt::renderer::vk {
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
// global functions
PFN_vkGetInstanceProcAddr vk_get_instance_proc_address;
PFN_vkCreateInstance vk_create_instance;
PFN_vkEnumerateInstanceExtensionProperties vk_enumerate_instance_extension_properties;
PFN_vkEnumerateInstanceLayerProperties vk_enumerate_instance_layer_properties;
// instance functions
PFN_vkDestroyInstance vk_destroy_instance;
PFN_vkEnumeratePhysicalDevices vk_enumerate_physical_devices;
PFN_vkGetPhysicalDeviceProperties vk_get_physical_device_properties;
PFN_vkGetPhysicalDeviceQueueFamilyProperties vk_get_physical_device_queue_family_properties;
PFN_vkCreateDevice vk_create_device;
PFN_vkGetDeviceProcAddr vk_get_device_proc_address;
PFN_vkDestroyDevice vk_destroy_device;
PFN_vkGetPhysicalDeviceFeatures vk_get_physical_device_features;
PFN_vkEnumerateDeviceExtensionProperties vk_enumerate_device_extension_properties;
// extension instance functions
PFN_vkCmdBeginDebugUtilsLabelEXT vk_cmd_begin_debug_label;
PFN_vkCmdEndDebugUtilsLabelEXT vk_cmd_end_debug_label;
PFN_vkCmdInsertDebugUtilsLabelEXT vk_cmd_insert_debug_label;
PFN_vkCreateDebugUtilsMessengerEXT vk_create_debug_messenger;
PFN_vkDestroyDebugUtilsMessengerEXT vk_destroy_debug_messenger;
PFN_vkQueueBeginDebugUtilsLabelEXT vk_queue_begin_debug_label;
PFN_vkQueueEndDebugUtilsLabelEXT vk_queue_end_debug_label;
PFN_vkQueueInsertDebugUtilsLabelEXT vk_queue_insert_debug_label;
PFN_vkSetDebugUtilsObjectNameEXT vk_set_debug_object_name;
PFN_vkSetDebugUtilsObjectTagEXT vk_set_debug_object_tag;
PFN_vkSubmitDebugUtilsMessageEXT vk_submit_debug_message;
// device functions
PFN_vkGetDeviceQueue vk_get_device_queue;
PFN_vkCreateCommandPool vk_create_command_pool;
PFN_vkDestroyCommandPool vk_destroy_command_pool;
PFN_vkAllocateCommandBuffers vk_allocate_command_buffers;
PFN_vkFreeCommandBuffers vk_free_command_buffers;
PFN_vkBeginCommandBuffer vk_begin_command_buffer;
PFN_vkEndCommandBuffer vk_end_command_buffer;
PFN_vkCmdPipelineBarrier vk_cmd_pipeline_barrier;
PFN_vkQueueSubmit vk_queue_submit;
PFN_vkQueueWaitIdle vk_queue_wait_idle;
PFN_vkDeviceWaitIdle vk_device_wait_idle;
PFN_vkCreateFence vk_create_fence;
PFN_vkDestroyFence vk_destroy_fence;
PFN_vkWaitForFences vk_wait_for_fences;
PFN_vkResetFences vk_reset_fences;
PFN_vkCreateSemaphore vk_create_semaphore;
PFN_vkDestroySemaphore vk_destroy_semaphore;
PFN_vkCreateSwapchainKHR vk_create_swapchain_khr;
PFN_vkDestroySwapchainKHR vk_destroy_swapchain_khr;
PFN_vkGetSwapchainImagesKHR vk_get_swapchain_images_khr;
PFN_vkAcquireNextImageKHR vk_acquire_next_image_khr;
PFN_vkQueuePresentKHR vk_queue_present_khr;
PFN_vkCreateImageView vk_create_image_view;
PFN_vkDestroyImageView vk_destroy_image_view;
PFN_vkCreateRenderPass vk_create_render_pass;
PFN_vkDestroyRenderPass vk_destroy_render_pass;
PFN_vkCreateFramebuffer vk_create_frame_buffer;
PFN_vkDestroyFramebuffer vk_destroy_frame_buffer;
PFN_vkCreateShaderModule vk_create_shader_module;
PFN_vkDestroyShaderModule vk_destroy_shader_module;
PFN_vkCreatePipelineLayout vk_create_pipeline_layout;
PFN_vkDestroyPipelineLayout vk_destroy_pipeline_layout;
PFN_vkCreateGraphicsPipelines vk_create_graphics_pipelines;
PFN_vkDestroyPipeline vk_destroy_pipeline;
PFN_vkCmdBeginRenderPass vk_cmd_begin_render_pass;
PFN_vkCmdEndRenderPass vk_cmd_end_render_pass;
PFN_vkCmdBindPipeline vk_cmd_bind_pipeline;
PFN_vkCmdDraw vk_cmd_draw;
PFN_vkCmdSetViewport vk_cmd_set_viewport;
PFN_vkCmdSetScissor vk_cmd_set_scissors;
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
Backend::Backend()
{
load_library();
load_global_functions();
initialize_instance();
load_instance_functions();
initialize_debug_messenger();
initialize_physical_device();
initialize_logical_device();
load_device_functions();
initialize_queue();
}
Backend::~Backend()
{
vk_destroy_device(m_device, nullptr);
vk_destroy_debug_messenger(m_instance, m_debug_messenger, nullptr);
vk_destroy_instance(m_instance, nullptr);
}
auto parse_message_type(VkDebugUtilsMessageTypeFlagsEXT message_types) -> const char *
{
if (message_types == VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT)
{
return "GENERAL";
}
if (message_types
== (VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT
| VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT))
{
return "VALIDATION | PERFORMANCE";
}
if (message_types == VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT)
{
return "VALIDATION";
}
return "PERFORMANCE";
}
auto parse_message_severity(VkDebugUtilsMessageSeverityFlagBitsEXT message_severity) -> LogLvl
{
switch (message_severity)
{
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT: return LogLvl::trace;
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT: return LogLvl::info;
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT: return LogLvl::warn;
case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT: return LogLvl::error;
default: ensure(false, "Invalid message severity: {}", static_cast<int>(message_severity));
}
return {};
}
auto validation_layers_callback(
VkDebugUtilsMessageSeverityFlagBitsEXT const message_severity,
VkDebugUtilsMessageTypeFlagsEXT const message_types,
VkDebugUtilsMessengerCallbackDataEXT const *const callback_data,
void *const vulkan_user_data
) -> VkBool32
{
std::ignore = vulkan_user_data;
const auto &type = parse_message_type(message_types);
const auto level = parse_message_severity(message_severity);
Logger::log(level, ":: <{}> :: {}", type, callback_data->pMessage);
return static_cast<VkBool32>(VK_FALSE);
}
void Backend::initialize_instance()
{
auto app_info = VkApplicationInfo {
.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
.pApplicationName = "Hallo Hallo Hallo :3",
.applicationVersion = VK_MAKE_VERSION(1, 4, 0),
.pEngineName = "light",
.engineVersion = VK_MAKE_VERSION(1, 4, 0),
.apiVersion = VK_API_VERSION_1_4,
};
auto extensions = std::vector<const char *> {
VK_EXT_DEBUG_UTILS_EXTENSION_NAME,
VK_KHR_SURFACE_EXTENSION_NAME,
"VK_KHR_xlib_surface",
};
auto layers = std::vector<const char *> {
"VK_LAYER_KHRONOS_validation",
};
auto instance_info = VkInstanceCreateInfo {
.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
.pApplicationInfo = &app_info,
.enabledLayerCount = static_cast<uint32_t>(layers.size()),
.ppEnabledLayerNames = layers.data(),
.enabledExtensionCount = static_cast<uint32_t>(extensions.size()),
.ppEnabledExtensionNames = extensions.data(),
};
{
auto count = 0u;
vk_enumerate_instance_extension_properties(nullptr, &count, nullptr);
auto extensions = std::vector<VkExtensionProperties>(count);
vk_enumerate_instance_extension_properties(nullptr, &count, extensions.data());
log_inf("Available vulkan instance extensions:");
for (auto &ext : extensions)
{
log_inf("\t{} @ {}", ext.extensionName, ext.specVersion);
}
}
vk_create_instance(&instance_info, nullptr, &m_instance);
ensure(m_instance, "Failed to create vulkan instance");
}
void Backend::initialize_debug_messenger()
{
const auto info = VkDebugUtilsMessengerCreateInfoEXT {
.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT,
.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT
| VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT
| VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT
| VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT,
.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT
| VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT
| VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT,
.pfnUserCallback = &validation_layers_callback,
};
ensure(
!vk_create_debug_messenger(m_instance, &info, nullptr, &m_debug_messenger),
"Failed to create vulkan debug utils messenger"
);
}
void Backend::initialize_physical_device()
{
auto count = 0u;
vk_enumerate_physical_devices(m_instance, &count, nullptr);
ensure(count != 0u, "Failed to find any physical devices with Vulkan support");
auto devices = std::vector<VkPhysicalDevice>(count);
vk_enumerate_physical_devices(m_instance, &count, devices.data());
for (auto &device : devices)
{
auto properties = VkPhysicalDeviceProperties {};
auto features = VkPhysicalDeviceFeatures {};
vk_get_physical_device_properties(device, &properties);
vk_get_physical_device_features(device, &features);
if (properties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU
&& features.geometryShader)
{
m_physical_device = device;
}
}
ensure(m_physical_device, "Failed to find any suitable Vulkan physical device");
}
void Backend::initialize_logical_device()
{
const float priorities = .0f;
auto queue_info = VkDeviceQueueCreateInfo {
.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
.queueFamilyIndex = find_suitable_queue_family(),
.queueCount = 1u,
.pQueuePriorities = &priorities,
};
auto physical_device_features = VkPhysicalDeviceFeatures {};
auto extensions = std::vector<const char *> {
VK_KHR_SWAPCHAIN_EXTENSION_NAME,
};
auto device_info = VkDeviceCreateInfo {
.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
.queueCreateInfoCount = 1,
.pQueueCreateInfos = &queue_info,
.enabledExtensionCount = static_cast<uint32_t>(extensions.size()),
.ppEnabledExtensionNames = extensions.data(),
.pEnabledFeatures = &physical_device_features,
};
ensure(
!vk_create_device(m_physical_device, &device_info, nullptr, &m_device),
"Failed to create logical vulkan device"
);
}
void Backend::initialize_queue()
{
vk_get_device_queue(m_device, find_suitable_queue_family(), 0, &m_queue);
}
void Backend::load_library()
{
library = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL);
ensure(library, "Failed to dlopen libvulkan.so");
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
vk_get_instance_proc_address = reinterpret_cast<PFN_vkGetInstanceProcAddr>(
dlsym(library, "vkGetInstanceProcAddr")
);
ensure(vk_get_instance_proc_address, "Failed to load vulkan function: vkGetInstanceProcAddr");
}
void Backend::load_global_functions()
{
constexpr auto load_fn = []<typename T>(T &pfn, const char *fn_name) {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
pfn = reinterpret_cast<T>(vk_get_instance_proc_address(nullptr, fn_name));
ensure(pfn, "Failed to load vulkan global function: {}", fn_name);
log_trc("Loaded global function: {}", fn_name);
};
load_fn(vk_create_instance, "vkCreateInstance");
load_fn(vk_enumerate_instance_extension_properties, "vkEnumerateInstanceExtensionProperties");
load_fn(vk_enumerate_instance_layer_properties, "vkEnumerateInstanceLayerProperties");
}
void Backend::load_instance_functions()
{
const auto load_fn = [&]<typename T>(T &pfn, const char *fn_name) {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
pfn = reinterpret_cast<T>(vk_get_instance_proc_address(m_instance, fn_name));
ensure(pfn, "Failed to load vulkan instance function: {}", fn_name);
log_trc("Loaded instance function: {}", fn_name);
};
load_fn(vk_destroy_instance, "vkDestroyInstance");
load_fn(vk_enumerate_physical_devices, "vkEnumeratePhysicalDevices");
load_fn(vk_get_physical_device_properties, "vkGetPhysicalDeviceProperties");
load_fn(
vk_get_physical_device_queue_family_properties,
"vkGetPhysicalDeviceQueueFamilyProperties"
);
load_fn(vk_create_device, "vkCreateDevice");
load_fn(vk_get_device_proc_address, "vkGetDeviceProcAddr");
load_fn(vk_destroy_device, "vkDestroyDevice");
load_fn(vk_get_physical_device_features, "vkGetPhysicalDeviceFeatures");
load_fn(vk_enumerate_device_extension_properties, "vkEnumerateDeviceExtensionProperties");
load_fn(vk_cmd_begin_debug_label, "vkCmdBeginDebugUtilsLabelEXT");
load_fn(vk_cmd_end_debug_label, "vkCmdEndDebugUtilsLabelEXT");
load_fn(vk_cmd_insert_debug_label, "vkCmdInsertDebugUtilsLabelEXT");
load_fn(vk_create_debug_messenger, "vkCreateDebugUtilsMessengerEXT");
load_fn(vk_destroy_debug_messenger, "vkDestroyDebugUtilsMessengerEXT");
load_fn(vk_queue_begin_debug_label, "vkQueueBeginDebugUtilsLabelEXT");
load_fn(vk_queue_end_debug_label, "vkQueueEndDebugUtilsLabelEXT");
load_fn(vk_queue_insert_debug_label, "vkQueueInsertDebugUtilsLabelEXT");
load_fn(vk_set_debug_object_name, "vkSetDebugUtilsObjectNameEXT");
load_fn(vk_set_debug_object_tag, "vkSetDebugUtilsObjectTagEXT");
load_fn(vk_submit_debug_message, "vkSubmitDebugUtilsMessageEXT");
}
void Backend::load_device_functions()
{
const auto load_fn = [&]<typename T>(T &pfn, const char *fn_name) {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
pfn = reinterpret_cast<T>(vk_get_device_proc_address(m_device, fn_name));
ensure(pfn, "Failed to load vulkan device function: {}", fn_name);
log_trc("Loaded device function: {}", fn_name);
};
load_fn(vk_get_device_queue, "vkGetDeviceQueue");
load_fn(vk_create_command_pool, "vkCreateCommandPool");
load_fn(vk_destroy_command_pool, "vkDestroyCommandPool");
load_fn(vk_allocate_command_buffers, "vkAllocateCommandBuffers");
load_fn(vk_free_command_buffers, "vkFreeCommandBuffers");
load_fn(vk_begin_command_buffer, "vkBeginCommandBuffer");
load_fn(vk_end_command_buffer, "vkEndCommandBuffer");
load_fn(vk_cmd_pipeline_barrier, "vkCmdPipelineBarrier");
load_fn(vk_queue_submit, "vkQueueSubmit");
load_fn(vk_queue_wait_idle, "vkQueueWaitIdle");
load_fn(vk_device_wait_idle, "vkDeviceWaitIdle");
load_fn(vk_create_fence, "vkCreateFence");
load_fn(vk_destroy_fence, "vkDestroyFence");
load_fn(vk_wait_for_fences, "vkWaitForFences");
load_fn(vk_reset_fences, "vkResetFences");
load_fn(vk_create_semaphore, "vkCreateSemaphore");
load_fn(vk_destroy_semaphore, "vkDestroySemaphore");
load_fn(vk_create_swapchain_khr, "vkCreateSwapchainKHR");
load_fn(vk_destroy_swapchain_khr, "vkDestroySwapchainKHR");
load_fn(vk_get_swapchain_images_khr, "vkGetSwapchainImagesKHR");
load_fn(vk_acquire_next_image_khr, "vkAcquireNextImageKHR");
load_fn(vk_queue_present_khr, "vkQueuePresentKHR");
load_fn(vk_create_image_view, "vkCreateImageView");
load_fn(vk_destroy_image_view, "vkDestroyImageView");
load_fn(vk_create_render_pass, "vkCreateRenderPass");
load_fn(vk_destroy_render_pass, "vkDestroyRenderPass");
load_fn(vk_create_frame_buffer, "vkCreateFramebuffer");
load_fn(vk_destroy_frame_buffer, "vkDestroyFramebuffer");
load_fn(vk_create_shader_module, "vkCreateShaderModule");
load_fn(vk_destroy_shader_module, "vkDestroyShaderModule");
load_fn(vk_create_pipeline_layout, "vkCreatePipelineLayout");
load_fn(vk_destroy_pipeline_layout, "vkDestroyPipelineLayout");
load_fn(vk_create_graphics_pipelines, "vkCreateGraphicsPipelines");
load_fn(vk_destroy_pipeline, "vkDestroyPipeline");
load_fn(vk_cmd_begin_render_pass, "vkCmdBeginRenderPass");
load_fn(vk_cmd_end_render_pass, "vkCmdEndRenderPass");
load_fn(vk_cmd_bind_pipeline, "vkCmdBindPipeline");
load_fn(vk_cmd_draw, "vkCmdDraw");
load_fn(vk_cmd_set_viewport, "vkCmdSetViewport");
load_fn(vk_cmd_set_scissors, "vkCmdSetScissor");
}
[[nodiscard]] auto Backend::find_suitable_queue_family() const -> uint32_t
{
auto count = 0u;
vk_get_physical_device_queue_family_properties(m_physical_device, &count, nullptr);
ensure(count != 0u, "Failed to find any physical devices with Vulkan support");
auto families = std::vector<VkQueueFamilyProperties>(count);
vk_get_physical_device_queue_family_properties(m_physical_device, &count, families.data());
const auto required_flags = VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT;
for (auto idx = 0u; auto &family : families)
{
if ((family.queueFlags & required_flags) == required_flags)
{
return idx;
}
}
ensure(false, "Failed to find a suitable Vulkan queue family");
return 0;
}
} // namespace lt::renderer::vk

View file

@ -0,0 +1,135 @@
#pragma once
#define VK_NO_PROTOTYPES
#include <renderer/backend.hpp>
#include <vulkan/vulkan.h>
namespace lt::renderer::vk {
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
// global functions
extern PFN_vkGetInstanceProcAddr vk_get_instance_proc_address;
extern PFN_vkCreateInstance vk_create_instance;
extern PFN_vkEnumerateInstanceExtensionProperties vk_enumerate_instance_extension_properties;
extern PFN_vkEnumerateInstanceLayerProperties vk_enumerate_instance_layer_properties;
// instance functions
extern PFN_vkDestroyInstance vk_destroy_instance;
extern PFN_vkEnumeratePhysicalDevices vk_enumerate_physical_devices;
extern PFN_vkGetPhysicalDeviceProperties vk_get_physical_device_properties;
extern PFN_vkGetPhysicalDeviceQueueFamilyProperties vk_get_physical_device_queue_family_properties;
extern PFN_vkCreateDevice vk_create_device;
extern PFN_vkGetDeviceProcAddr vk_get_device_proc_address;
extern PFN_vkDestroyDevice vk_destroy_device;
extern PFN_vkGetPhysicalDeviceFeatures vk_get_physical_device_features;
extern PFN_vkEnumerateDeviceExtensionProperties vk_enumerate_device_extension_properties;
// extension instance functions
extern PFN_vkCmdBeginDebugUtilsLabelEXT vk_cmd_begin_debug_label;
extern PFN_vkCmdEndDebugUtilsLabelEXT vk_cmd_end_debug_label;
extern PFN_vkCmdInsertDebugUtilsLabelEXT vk_cmd_insert_debug_label;
extern PFN_vkCreateDebugUtilsMessengerEXT vk_create_debug_messenger;
extern PFN_vkDestroyDebugUtilsMessengerEXT vk_destroy_debug_messenger;
extern PFN_vkQueueBeginDebugUtilsLabelEXT vk_queue_begin_debug_label;
extern PFN_vkQueueEndDebugUtilsLabelEXT vk_queue_end_debug_label;
extern PFN_vkQueueInsertDebugUtilsLabelEXT vk_queue_insert_debug_label;
extern PFN_vkSetDebugUtilsObjectNameEXT vk_set_debug_object_name;
extern PFN_vkSetDebugUtilsObjectTagEXT vk_set_debug_object_tag;
extern PFN_vkSubmitDebugUtilsMessageEXT vk_submit_debug_message;
// device functions
extern PFN_vkGetDeviceQueue vk_get_device_queue;
extern PFN_vkCreateCommandPool vk_create_command_pool;
extern PFN_vkDestroyCommandPool vk_destroy_command_pool;
extern PFN_vkAllocateCommandBuffers vk_allocate_command_buffers;
extern PFN_vkFreeCommandBuffers vk_free_command_buffers;
extern PFN_vkBeginCommandBuffer vk_begin_command_buffer;
extern PFN_vkEndCommandBuffer vk_end_command_buffer;
extern PFN_vkCmdPipelineBarrier vk_cmd_pipeline_barrier;
extern PFN_vkQueueSubmit vk_queue_submit;
extern PFN_vkQueueWaitIdle vk_queue_wait_idle;
extern PFN_vkDeviceWaitIdle vk_device_wait_idle;
extern PFN_vkCreateFence vk_create_fence;
extern PFN_vkDestroyFence vk_destroy_fence;
extern PFN_vkWaitForFences vk_wait_for_fences;
extern PFN_vkResetFences vk_reset_fences;
extern PFN_vkCreateSemaphore vk_create_semaphore;
extern PFN_vkDestroySemaphore vk_destroy_semaphore;
extern PFN_vkCreateSwapchainKHR vk_create_swapchain_khr;
extern PFN_vkDestroySwapchainKHR vk_destroy_swapchain_khr;
extern PFN_vkGetSwapchainImagesKHR vk_get_swapchain_images_khr;
extern PFN_vkAcquireNextImageKHR vk_acquire_next_image_khr;
extern PFN_vkQueuePresentKHR vk_queue_present_khr;
extern PFN_vkCreateImageView vk_create_image_view;
extern PFN_vkDestroyImageView vk_destroy_image_view;
extern PFN_vkCreateRenderPass vk_create_render_pass;
extern PFN_vkDestroyRenderPass vk_destroy_render_pass;
extern PFN_vkCreateFramebuffer vk_create_frame_buffer;
extern PFN_vkDestroyFramebuffer vk_destroy_frame_buffer;
extern PFN_vkCreateShaderModule vk_create_shader_module;
extern PFN_vkDestroyShaderModule vk_destroy_shader_module;
extern PFN_vkCreatePipelineLayout vk_create_pipeline_layout;
extern PFN_vkDestroyPipelineLayout vk_destroy_pipeline_layout;
extern PFN_vkCreateGraphicsPipelines vk_create_graphics_pipelines;
extern PFN_vkDestroyPipeline vk_destroy_pipeline;
extern PFN_vkCmdBeginRenderPass vk_cmd_begin_render_pass;
extern PFN_vkCmdEndRenderPass vk_cmd_end_render_pass;
extern PFN_vkCmdBindPipeline vk_cmd_bind_pipeline;
extern PFN_vkCmdDraw vk_cmd_draw;
extern PFN_vkCmdSetViewport vk_cmd_set_viewport;
extern PFN_vkCmdSetScissor vk_cmd_set_scissors;
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
class Backend: public ::lt::renderer::Backend
{
public:
Backend();
Backend(Backend &&) = default;
auto operator=(Backend &&) -> Backend & = default;
Backend(const Backend &) = delete;
auto operator=(const Backend &) -> Backend & = delete;
~Backend() override;
[[nodiscard]] constexpr auto get_api() const -> API override
{
return API::vulkan;
}
private:
void initialize_instance();
void initialize_debug_messenger();
void initialize_physical_device();
void initialize_logical_device();
void initialize_queue();
void load_library();
void load_global_functions();
void load_instance_functions();
void load_device_functions();
[[nodiscard]] auto find_suitable_queue_family() const -> uint32_t;
VkInstance m_instance {};
VkPhysicalDevice m_physical_device {};
VkDevice m_device {};
VkQueue m_queue {};
VkDebugUtilsMessengerEXT m_debug_messenger {};
};
} // namespace lt::renderer::vk

View file

@ -0,0 +1,5 @@
#include <renderer/vk/instance.hpp>
namespace lt::vk {
}

View file

@ -0,0 +1,19 @@
#pragma once
#include <vulkan/vulkan.h>
#include <vulkan/vulkan.hpp>
namespace lt::vk {
class Instance
{
public:
Instance()
{
}
private:
::vk::Instance m_instace;
};
} // namespace lt::vk

View file

@ -0,0 +1,8 @@
#include <renderer/vk/instance.hpp>
#include <test/test.hpp>
lt::test::Suite raii = [] {
lt::test::Case { "raii" } = [] {
auto instance = lt::vk::Instance {};
};
};

View file

@ -0,0 +1,29 @@
#pragma once
namespace lt::renderer {
class Backend
{
public:
enum class API : uint8_t
{
vulkan,
directx,
};
Backend() = default;
Backend(Backend &&) = default;
auto operator=(Backend &&) -> Backend & = default;
Backend(const Backend &) = delete;
auto operator=(const Backend &) -> Backend & = delete;
virtual ~Backend() = default;
[[nodiscard]] virtual auto get_api() const -> API = 0;
};
} // namespace lt::renderer

View file

@ -0,0 +1,12 @@
#pragma once
namespace lt::renderer {
/** Requires a Transform Component
* @todo(Light): Figure out how to enforce a component requirement list for a component
*/
struct RendererComponent
{
};
} // namespace lt::renderer

View file

@ -0,0 +1,9 @@
#pragma once
namespace lt::renderer {
class Context
{
};
} // namespace lt::renderer

View file

@ -1,6 +1,5 @@
#pragma once
//
#include <math/mat4.hpp>
#include <math/vec3.hpp>
#include <math/vec4.hpp>
@ -8,11 +7,6 @@
#include <renderer/buffers.hpp>
#include <renderer/render_command.hpp>
#include <renderer/renderer.hpp>
///
#include <renderer/programs/quad.hpp>
#include <renderer/programs/texture.hpp>
#include <renderer/programs/tinted_texture.hpp>
#include <utility>
#define LT_MAX_QUAD_RENDERER_VERTICES (1028u * 4u)
@ -32,6 +26,10 @@ class Camera;
class WindowResizedEvent;
class Shader;
class TintedTextureRendererProgram;
class QuadRendererProgram;
class TextureRendererProgram;
class Renderer
{
public:
@ -107,6 +105,8 @@ public:
s_context->end_scene_impl();
}
~Renderer();
void on_window_resize(const WindowResizedEvent &event);
void begin_frame();
@ -116,11 +116,11 @@ public:
private:
static Renderer *s_context;
QuadRendererProgram m_quad_renderer;
Scope<QuadRendererProgram> m_quad_renderer;
TextureRendererProgram m_texture_renderer;
Scope<TextureRendererProgram> m_texture_renderer;
TintedTextureRendererProgram m_tinted_texture_renderer;
Scope<TintedTextureRendererProgram> m_tinted_texture_renderer;
Scope<ConstantBuffer> m_view_projection_buffer;

View file

@ -0,0 +1,60 @@
#pragma once
#include <base/base.hpp>
#include <ecs/scene.hpp>
struct GLFWwindow;
namespace lt::renderer {
/** The system for putting gore on your display
*
* Exclusively operates on components:
* - RendererComponent
* - PostEffectsComponent
* - UserInterfaceComponent
*
* Requires read acces on components:
* - TransformComponent
*/
class System
{
public:
/** The configurations of this system. */
struct Properties
{
};
/** The requirements for this system to initialize. */
struct InitRequirements
{
GLFWwindow *glfw_window_handle;
Ref<ecs::Registry> registry;
};
/** The requirements for this system to tick. */
struct TickRequirements
{
double delta_time;
};
[[nodiscard]] System(InitRequirements requirements);
System(System &&) = default;
System(const System &) = delete;
auto operator=(System &&) -> System & = default;
auto operator=(const System &) -> System & = delete;
~System();
void tick(TickRequirements requirements);
private:
Ref<ecs::Registry> m_registry;
};
} // namespace lt::renderer

View file

@ -0,0 +1,16 @@
if (NOT WIN32)
add_library_module(surface linux/system.cpp)
else()
endif()
target_link_libraries(surface PUBLIC
ecs
app
PRIVATE
glfw
logger
lt_debug
)
add_test_module(surface system.test.cpp)
add_fuzz_module(surface system.fuzz.cpp)

View file

@ -0,0 +1 @@
1484824981238913982498139812098u24

View file

@ -0,0 +1,292 @@
#define GLFW_EXPOSE_NATIVE_X11
#include <GLFW/glfw3.h>
#include <GLFW/glfw3native.h>
#include <surface/system.hpp>
namespace lt::surface {
void glfw_error_callbac(int32_t code, const char *description)
{
log_err("GLFW ERROR: {} -> {}", code, description);
}
void handle_event(GLFWwindow *window, const SurfaceComponent::Event &event)
{
auto &callbacks = *static_cast<std::vector<SurfaceComponent::EventCallback> *>(
glfwGetWindowUserPointer(window)
);
for (auto &callback : callbacks)
{
if (callback(event))
{
return;
}
}
}
void bind_glfw_events(GLFWwindow *handle)
{
glfwSetWindowPosCallback(handle, [](GLFWwindow *window, int xpos, int ypos) {
handle_event(window, MovedEvent { xpos, ypos });
});
glfwSetWindowSizeCallback(handle, [](GLFWwindow *window, int width, int height) {
handle_event(
window,
ResizedEvent { static_cast<uint32_t>(width), static_cast<uint32_t>(height) }
);
});
glfwSetWindowCloseCallback(handle, [](GLFWwindow *window) {
handle_event(window, ClosedEvent {});
});
glfwSetWindowFocusCallback(handle, [](GLFWwindow *window, int focus) {
if (focus == GLFW_TRUE)
{
handle_event(window, GainFocusEvent {});
}
else
{
handle_event(window, LostFocusEvent {});
}
});
glfwSetCursorPosCallback(handle, [](GLFWwindow *window, double xpos, double ypos) {
handle_event(
window,
MouseMovedEvent { static_cast<float>(xpos), static_cast<float>(ypos) }
);
});
glfwSetMouseButtonCallback(
handle,
[](GLFWwindow *window, int button, int action, int /*mods*/) {
if (action == GLFW_PRESS)
{
handle_event(window, ButtonPressedEvent { button });
}
else if (action == GLFW_RELEASE)
{
handle_event(window, ButtonReleasedEvent { button });
}
}
);
glfwSetScrollCallback(handle, [](GLFWwindow *window, double /*xoffset*/, double yoffset) {
handle_event(window, WheelScrolledEvent { static_cast<float>(yoffset) });
});
glfwSetKeyCallback(
handle,
[](GLFWwindow *window, int key, int /*scancode*/, int action, int /*mods*/) {
if (action == GLFW_PRESS)
{
handle_event(window, KeyPressedEvent { key });
}
else if (action == GLFW_RELEASE)
{
handle_event(window, KeyReleasedEvent { key });
}
}
);
glfwSetCharCallback(handle, [](GLFWwindow *window, unsigned int character) {
handle_event(window, KeySetCharEvent { character });
});
}
void init_glfw() {};
System::System(Ref<ecs::Registry> registry): m_registry(std::move(registry))
{
glfwSetErrorCallback(&glfw_error_callbac);
ensure(glfwInit(), "Failed to initialize 'glfw'");
ensure(m_registry, "Failed to initialize surface system: null registry");
ensure(
m_registry->view<SurfaceComponent>().size() == 0,
"Failed to initialize surface system: registry has surface component(s)"
);
m_registry->get_entt_registry()
.on_construct<SurfaceComponent>()
.connect<&System::on_surface_construct>(this);
m_registry->get_entt_registry()
.on_update<SurfaceComponent>()
.connect<&System::on_surface_update>(this);
m_registry->get_entt_registry()
.on_destroy<SurfaceComponent>()
.connect<&System::on_surface_destroy>(this);
}
System::~System()
{
m_registry->get_entt_registry()
.on_construct<SurfaceComponent>()
.disconnect<&System::on_surface_construct>(this);
m_registry->get_entt_registry()
.on_update<SurfaceComponent>()
.connect<&System::on_surface_update>(this);
m_registry->get_entt_registry()
.on_destroy<SurfaceComponent>()
.disconnect<&System::on_surface_destroy>(this);
m_registry->view<SurfaceComponent>().each([&](const entt::entity entity, SurfaceComponent &) {
m_registry->get_entt_registry().remove<SurfaceComponent>(entity);
});
glfwTerminate();
}
void System::on_surface_construct(entt::registry &registry, entt::entity entity)
{
try
{
auto &surface = registry.get<SurfaceComponent>(entity);
ensure_component_sanity(surface);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
surface.m_glfw_handle = glfwCreateWindow(
static_cast<int>(surface.get_resolution().x),
static_cast<int>(surface.get_resolution().y),
surface.get_title().begin(),
nullptr,
nullptr
);
ensure(surface.m_glfw_handle, "Failed to create 'GLFWwindow'");
glfwSetWindowUserPointer(surface.m_glfw_handle, &surface.m_event_callbacks);
surface.m_native_handle = glfwGetX11Window(surface.m_glfw_handle);
bind_glfw_events(surface.m_glfw_handle);
}
catch (...)
{
registry.remove<SurfaceComponent>(entity);
throw;
}
}
void System::on_surface_update(entt::registry &registry, entt::entity entity)
{
auto &surface = registry.get<SurfaceComponent>(entity);
glfwSetWindowUserPointer(surface.m_glfw_handle, &surface.m_event_callbacks);
}
void System::on_surface_destroy(entt::registry &registry, entt::entity entity)
{
auto &surface = registry.get<SurfaceComponent>(entity);
if (surface.m_glfw_handle)
{
glfwDestroyWindow(surface.m_glfw_handle);
}
}
void System::set_title(ecs::Entity entity, std::string_view new_title)
{
auto &surface = entity.get_component<SurfaceComponent>();
surface.m_title = new_title;
glfwSetWindowTitle(surface.m_glfw_handle, surface.m_title.c_str());
}
auto System::tick() -> bool
{
m_registry->view<SurfaceComponent>().each([](SurfaceComponent &surface) {
glfwSwapBuffers(surface.m_glfw_handle);
});
glfwPollEvents();
return false;
}
void System::set_size(ecs::Entity surface_entity, const math::uvec2 &new_size)
{
auto &surface = surface_entity.get_component<SurfaceComponent>();
surface.m_resolution = new_size;
glfwSetWindowSize(
surface.m_glfw_handle,
static_cast<int>(new_size.x),
static_cast<int>(new_size.y)
);
}
void System::set_v_sync(ecs::Entity surface_entity, bool vsync)
{
auto &surface = surface_entity.get_component<SurfaceComponent>();
surface.m_vsync = vsync;
glfwSwapInterval(vsync);
}
void System::set_visibility(ecs::Entity surface_entity, bool visible)
{
auto &surface = surface_entity.get_component<SurfaceComponent>();
surface.m_visible = visible;
if (visible)
{
glfwShowWindow(surface.m_glfw_handle);
}
else
{
glfwHideWindow(surface.m_glfw_handle);
}
}
void System::add_event_listener(
ecs::Entity surface_entity,
SurfaceComponent::EventCallback callback
)
{
auto &surface = surface_entity.get_component<SurfaceComponent>();
surface.m_event_callbacks.emplace_back(std::move(callback));
}
void System::ensure_component_sanity(const SurfaceComponent &component)
{
auto [width, height] = component.get_resolution();
ensure(width != 0u, "Received bad values for surface component: width({}) == 0", width);
ensure(height != 0u, "Received bad values for surface component: height({}) == 0", height);
ensure(
width < SurfaceComponent::max_dimension,
"Received bad values for surface component: width({}) > max_dimension({})",
width,
SurfaceComponent::max_dimension
);
ensure(
height < SurfaceComponent::max_dimension,
"Received bad values for surface component: height({}) > max_dimension({})",
height,
SurfaceComponent::max_dimension
);
ensure(
component.get_title().size() < SurfaceComponent::max_title_length,
"Received bad values for surface component: title.size({}) > max_title_length({})",
component.get_title().size(),
SurfaceComponent::max_title_length
);
}
} // namespace lt::surface
namespace lt {
} // namespace lt

View file

@ -0,0 +1,103 @@
#include <ecs/scene.hpp>
#include <surface/system.hpp>
#include <test/fuzz.hpp>
#include <test/test.hpp>
namespace lt::surface {
enum class Action : uint8_t
{
create_entity,
create_surface_component,
destroy_surface_component,
tick,
};
void create_surface_component(test::FuzzDataProvider &provider, ecs::Registry &registry)
{
const auto length = std::min(provider.consume<uint32_t>().value_or(16), 255u);
const auto title = provider.consume_string(length).value_or("");
const auto resolution = math::uvec2 {
provider.consume<uint32_t>().value_or(32u),
provider.consume<uint32_t>().value_or(64u),
};
const auto visible = provider.consume<bool>().value_or(false);
const auto vsync = provider.consume<bool>().value_or(false);
try
{
registry.create_entity("").add_component<surface::SurfaceComponent>(
surface::SurfaceComponent::CreateInfo {
.title = std::move(title),
.resolution = resolution,
.vsync = vsync,
.visible = visible,
}
);
}
catch (const std::exception &exp)
{
std::ignore = exp;
}
}
void remove_surface_component(ecs::Registry &registry)
{
const auto view = registry.get_entt_registry().view<SurfaceComponent>();
if (!view->empty())
{
registry.get_entt_registry().remove<SurfaceComponent>(*view.begin());
}
}
void check_invariants()
{
}
test::FuzzHarness harness = [](const uint8_t *data, size_t size) {
auto provider = test::FuzzDataProvider { data, size };
auto registry = create_ref<ecs::Registry>();
auto system = surface::System { registry };
while (auto action = provider.consume<uint8_t>())
{
switch (static_cast<Action>(action.value()))
{
case Action::create_entity:
{
const auto length = std::min(provider.consume<uint32_t>().value_or(16), 255u);
const auto tag = provider.consume_string(length).value_or("");
registry->create_entity(tag);
break;
}
case Action::create_surface_component:
{
create_surface_component(provider, *registry);
break;
}
case Action::destroy_surface_component:
{
remove_surface_component(*registry);
break;
}
case Action::tick:
{
system.tick();
break;
}
}
check_invariants();
}
return 0;
};
} // namespace lt::surface

View file

@ -0,0 +1,198 @@
#include <surface/system.hpp>
#include <test/test.hpp>
using namespace lt;
using std::ignore;
using surface::SurfaceComponent;
using surface::System;
using test::Case;
using test::expect_eq;
using test::expect_ne;
using test::expect_not_nullptr;
using test::expect_throw;
using test::Suite;
constexpr auto title = "TestWindow";
constexpr auto width = 800u;
constexpr auto height = 600u;
constexpr auto vsync = true;
constexpr auto visible = false;
class Fixture
{
public:
[[nodiscard]] auto registry() -> Ref<ecs::Registry>
{
return m_registry;
}
auto add_surface_component(
SurfaceComponent::CreateInfo info = SurfaceComponent::CreateInfo {
.title = title,
.resolution = { width, height },
.vsync = vsync,
.visible = visible,
}
) -> SurfaceComponent &
{
auto entity = m_registry->create_entity("");
return entity.add_component<SurfaceComponent>(info);
}
void check_values(const SurfaceComponent &component)
{
expect_ne(std::get<SurfaceComponent::X11NativeHandle>(component.get_native_handle()), 0);
expect_eq(component.get_resolution().x, width);
expect_eq(component.get_resolution().y, height);
expect_eq(component.get_title(), title);
expect_eq(component.is_vsync(), vsync);
expect_eq(component.is_visible(), visible);
}
private:
Ref<ecs::Registry> m_registry = create_ref<ecs::Registry>();
};
Suite raii = [] {
Case { "happy path won't throw" } = [] {
auto fixture = Fixture {};
ignore = System { fixture.registry() };
};
Case { "many won't freeze/throw" } = [] {
auto fixture = Fixture {};
/* range is small since glfw init/terminate is slow. */
for (auto idx : std::views::iota(0, 100))
{
ignore = System { fixture.registry() };
}
};
Case { "unhappy path throws" } = [] {
expect_throw([] { ignore = System { {} }; });
auto fixture = Fixture {};
fixture.add_surface_component();
expect_throw([&] { ignore = System { { fixture.registry() } }; });
};
Case { "post construct has correct state" } = [] {
auto fixture = Fixture {};
auto system = System { fixture.registry() };
expect_eq(fixture.registry()->view<SurfaceComponent>()->size(), 0);
};
Case { "post destruct has correct state" } = [] {
auto fixture = Fixture {};
auto system = create_scope<System>(fixture.registry());
fixture.add_surface_component();
expect_eq(fixture.registry()->view<SurfaceComponent>()->size(), 1);
system.reset();
expect_eq(fixture.registry()->view<SurfaceComponent>()->size(), 0);
};
};
Suite system_events = [] {
Case { "on_register won't throw" } = [] {
auto fixture = Fixture {};
auto system = System { fixture.registry() };
system.on_register();
expect_eq(fixture.registry()->view<SurfaceComponent>().size(), 0);
};
Case { "on_unregister won't throw" } = [] {
auto fixture = Fixture {};
auto system = System { fixture.registry() };
system.on_register();
system.on_unregister();
expect_eq(fixture.registry()->view<SurfaceComponent>().size(), 0);
};
};
Suite registry_events = [] {
Case { "on_construct<SurfaceComponent> initializes component" } = [] {
auto fixture = Fixture {};
auto system = System { fixture.registry() };
const auto &component = fixture.add_surface_component();
expect_eq(fixture.registry()->view<SurfaceComponent>().size(), 1);
fixture.check_values(component);
};
Case { "unhappy on_construct<SurfaceComponent> throws" } = [] {
auto fixture = Fixture {};
auto system = System { fixture.registry() };
expect_throw([&] { fixture.add_surface_component({ .resolution = { width, 0 } }); });
expect_throw([&] { fixture.add_surface_component({ .resolution = { 0, height } }); });
expect_throw([&] {
fixture.add_surface_component(
{ .title = "", .resolution = { SurfaceComponent::max_dimension + 1, height } }
);
});
expect_throw([&] {
fixture.add_surface_component(
{ .title = "", .resolution = { width, SurfaceComponent::max_dimension + 1 } }
);
});
auto big_str = std::string {};
big_str.resize(SurfaceComponent::max_title_length + 1);
expect_throw([&] {
fixture.add_surface_component({ .title = big_str, .resolution = { width, height } });
});
};
Case { "unhappy on_construct<SurfaceComponent> removes component" } = [] {
auto fixture = Fixture {};
auto system = System { fixture.registry() };
expect_throw([&] { fixture.add_surface_component({ .resolution = { width, 0 } }); });
expect_eq(fixture.registry()->view<SurfaceComponent>().size(), 0);
};
Case { "on_destrroy<SurfaceComponent> cleans up component" } = [] {
auto fixture = Fixture {};
auto system = create_scope<System>(fixture.registry());
const auto &component = fixture.add_surface_component();
expect_eq(fixture.registry()->view<SurfaceComponent>().size(), 1);
fixture.check_values(component);
system.reset();
expect_eq(fixture.registry()->view<SurfaceComponent>().size(), 0);
};
};
Suite tick = [] {
Case { "ticking on empty registry won't throw" } = [] {
auto fixture = Fixture {};
System { fixture.registry() }.tick();
};
Case { "ticking on non-empty registry won't throw" } = [] {
auto fixture = Fixture {};
auto system = System { fixture.registry() };
fixture.add_surface_component();
system.tick();
};
Case { "ticking on chaotic registry won't throw" } = [] {
};
};
Suite property_setters = [] {
};
Suite listeners = [] {
};

View file

@ -0,0 +1,119 @@
#pragma once
#include <math/vec2.hpp>
#include <surface/events/keyboard.hpp>
#include <surface/events/mouse.hpp>
#include <surface/events/surface.hpp>
#include <variant>
struct GLFWwindow;
namespace lt::surface {
/** Represents a platform's surface (eg. a Window).
*
* @note Read-only component, should only be modified through a system.
*/
class SurfaceComponent
{
public:
friend class System;
using Event = std::variant<
// surface events
ClosedEvent,
MovedEvent,
ResizedEvent,
LostFocusEvent,
GainFocusEvent,
// keyboard events
KeyPressedEvent,
KeyRepeatEvent,
KeyReleasedEvent,
KeySetCharEvent,
// mouse events
MouseMovedEvent,
WheelScrolledEvent,
ButtonPressedEvent,
ButtonReleasedEvent>;
using EventCallback = std::function<bool(const Event &)>;
using WindowsNativeHandle = void *;
using X11NativeHandle = unsigned long;
using NativeHandle = std::variant<WindowsNativeHandle, X11NativeHandle>;
static constexpr auto max_dimension = 4096;
static constexpr auto max_title_length = 256;
struct CreateInfo
{
std::string_view title;
math::uvec2 resolution;
bool vsync;
bool visible;
};
SurfaceComponent(const CreateInfo &info)
: m_title(info.title)
, m_resolution(info.resolution)
, m_vsync(info.vsync)
, m_visible(info.visible)
{
}
[[nodiscard]] auto get_title() const -> std::string_view
{
return m_title;
}
[[nodiscard]] auto get_resolution() const -> const math::uvec2 &
{
return m_resolution;
}
[[nodiscard]] auto is_vsync() const -> bool
{
return m_vsync;
}
[[nodiscard]] auto is_visible() const -> bool
{
return m_visible;
}
[[nodiscard]] auto get_native_handle() const -> NativeHandle
{
return m_native_handle;
}
private:
[[nodiscard]] auto get_glfw_handle() const -> GLFWwindow *
{
return m_glfw_handle;
}
std::string m_title;
math::uvec2 m_resolution;
bool m_vsync;
bool m_visible;
NativeHandle m_native_handle;
GLFWwindow *m_glfw_handle {};
std::vector<EventCallback> m_event_callbacks;
};
} // namespace lt::surface

View file

@ -0,0 +1,91 @@
#pragma once
#include <format>
namespace lt::surface {
class KeyPressedEvent
{
public:
KeyPressedEvent(int32_t key): m_key(key)
{
}
[[nodiscard]] auto get_key() const -> int32_t
{
return m_key;
}
[[nodiscard]] auto to_string() const -> std::string
{
return std::format("KeyPressed: {}", m_key);
}
private:
int32_t m_key;
};
class KeyRepeatEvent
{
public:
KeyRepeatEvent(int key): m_key(key)
{
}
[[nodiscard]] auto get_key() const -> int32_t
{
return m_key;
}
[[nodiscard]] auto to_string() const -> std::string
{
return std::format("KeyRepeated: {}", m_key);
}
private:
int32_t m_key;
};
class KeyReleasedEvent
{
public:
KeyReleasedEvent(int key): m_key(key)
{
}
[[nodiscard]] auto get_key() const -> int32_t
{
return m_key;
}
[[nodiscard]] auto to_string() const -> std::string
{
return std::format("KeyReleased: {}", m_key);
}
private:
int32_t m_key;
};
class KeySetCharEvent
{
public:
KeySetCharEvent(uint32_t character): m_character(character)
{
}
[[nodiscard]] auto get_character() const -> uint32_t
{
return m_character;
}
[[nodiscard]] auto to_string() const -> std::string
{
return std::format("KeyCharSet: {}", m_character);
}
private:
uint32_t m_character;
};
} // namespace lt::surface

View file

@ -0,0 +1,103 @@
#pragma once
#include <math/vec2.hpp>
namespace lt::surface {
class MouseMovedEvent
{
public:
MouseMovedEvent(float x, float y): m_position(x, y)
{
}
[[nodiscard]] auto get_position() const -> const math::vec2 &
{
return m_position;
}
[[nodiscard]] auto get_x() const -> float
{
return m_position.x;
}
[[nodiscard]] auto get_y() const -> float
{
return m_position.y;
}
[[nodiscard]] auto to_string() const -> std::string
{
return std::format("MouseMoved: {}, {}", m_position.x, m_position.y);
}
private:
math::vec2 m_position;
};
class WheelScrolledEvent
{
public:
WheelScrolledEvent(float offset): m_offset(offset)
{
}
[[nodiscard]] auto get_offset() const -> float
{
return m_offset;
}
[[nodiscard]] auto to_string() const -> std::string
{
std::stringstream ss;
ss << "WheelScrolled: " << m_offset;
return ss.str();
}
private:
float m_offset;
};
class ButtonPressedEvent
{
public:
ButtonPressedEvent(int button): m_button(button)
{
}
[[nodiscard]] auto get_button() const -> int
{
return m_button;
}
[[nodiscard]] auto to_string() const -> std::string
{
return std::format("ButtonPressed: {}", m_button);
}
private:
int m_button;
};
class ButtonReleasedEvent
{
public:
ButtonReleasedEvent(int button): m_button(button)
{
}
[[nodiscard]] auto get_button() const -> int
{
return m_button;
}
[[nodiscard]] auto to_string() const -> std::string
{
return std::format("ButtonReleased: {}", m_button);
}
private:
int m_button;
};
} // namespace lt::surface

View file

@ -0,0 +1,81 @@
#pragma once
#include <math/vec2.hpp>
#include <sstream>
namespace lt::surface {
class ClosedEvent
{
public:
[[nodiscard]] auto to_string() const -> std::string_view
{
return "SurfaceClosedEvent";
}
};
class MovedEvent
{
public:
MovedEvent(int x, int y): m_position(x, y)
{
}
[[nodiscard]] auto get_position() const -> const math::ivec2 &
{
return m_position;
}
[[nodiscard]] auto to_string() const -> std::string
{
auto stream = std::stringstream {};
stream << "WindwoMoved: " << m_position.x << ", " << m_position.y;
return stream.str();
}
private:
math::ivec2 m_position;
};
class ResizedEvent
{
public:
ResizedEvent(unsigned int width, unsigned int height): m_size(width, height)
{
}
[[nodiscard]] auto get_size() const -> const math::uvec2 &
{
return m_size;
}
[[nodiscard]] auto to_string() const -> std::string
{
auto stream = std::stringstream {};
stream << "SurfaceResized: " << m_size.x << ", " << m_size.y;
return stream.str();
}
private:
math::uvec2 m_size;
};
class LostFocusEvent
{
public:
[[nodiscard]] auto to_string() const -> std::string_view
{
return "SurfaceLostFocus";
}
};
class GainFocusEvent
{
public:
[[nodiscard]] auto to_string() const -> std::string_view
{
return "SurfaceGainFocus";
}
};
} // namespace lt::surface

View file

@ -0,0 +1,58 @@
#pragma once
#include <app/system.hpp>
#include <ecs/entity.hpp>
#include <ecs/scene.hpp>
#include <surface/components.hpp>
namespace lt::surface {
class System: public app::ISystem
{
public:
[[nodiscard]] System(Ref<ecs::Registry> registry);
~System() override;
System(System &&) = default;
System(const System &) = delete;
auto operator=(System &&) -> System & = default;
auto operator=(const System &) -> System & = delete;
void on_register() override
{
}
void on_unregister() override
{
}
auto tick() -> bool override;
static void set_title(ecs::Entity surface_entity, std::string_view new_title);
void set_size(ecs::Entity surface_entity, const math::uvec2 &new_size);
void set_v_sync(ecs::Entity surface_entity, bool vsync);
void set_visibility(ecs::Entity surface_entity, bool visible);
void add_event_listener(ecs::Entity surface_entity, SurfaceComponent::EventCallback callback);
private:
void on_surface_construct(entt::registry &registry, entt::entity entity);
void on_surface_update(entt::registry &registry, entt::entity entity);
void on_surface_destroy(entt::registry &registry, entt::entity entity);
void ensure_component_sanity(const SurfaceComponent &component);
Ref<ecs::Registry> m_registry;
};
} // namespace lt::surface

View file

@ -1,2 +1,4 @@
add_library_module(test test.cpp entrypoint.cpp)
add_test_module(test test.tests.cpp)
add_library_module(fuzz_test test.cpp fuzz.cpp)
add_test_module(test test.test.cpp)

View file

@ -0,0 +1,24 @@
#include <test/test.hpp>
namespace lt::test {
auto process_fuzz_input(const uint8_t *data, size_t size) -> int32_t
try
{
return details::Registry::process_fuzz_input(data, size);
}
catch (const std::exception &exp)
{
std::cout << "Fuzz input resulted in uncaught exception:\n";
std::cout << "\texception.what: " << exp.what() << '\n';
std::cout << "\tinput size: " << size << '\n';
return EXIT_FAILURE;
}
}; // namespace lt::test
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
return lt::test::process_fuzz_input(data, size);
}

View file

@ -0,0 +1,199 @@
#include <test/test.hpp>
using lt::test::Case;
using lt::test::Suite;
Suite expects = []() {
using lt::test::expect_unreachable;
using lt::test::expect_true;
using lt::test::expect_false;
using lt::test::expect_eq;
using lt::test::expect_ne;
using lt::test::expect_le;
using lt::test::expect_throw;
Case { "" } = [] {
};
Case { "expect_unreachable" } = [] {
auto unhappy = false;
// clang-format off
try { expect_unreachable(); }
catch (const std::exception &exp) { unhappy = true; }
// clang-format on
if (!unhappy)
{
throw std::runtime_error { "expect_unreachable" };
}
};
Case { "expect_true - happy" } = [] {
auto oongaboonga = int {};
auto *oongaboonga_ptr_here = &oongaboonga;
expect_true(oongaboonga_ptr_here);
expect_true(true);
expect_true(1); // NOLINT
};
Case { "expect_true - unhappy" } = [] {
auto unhappy_counter = 0u;
auto *where_oongaboonga_ptr = (int *)nullptr;
// clang-format off
try { expect_true(where_oongaboonga_ptr); }
catch (const std::exception& exp) { ++unhappy_counter; }
try { expect_true(!true); }
catch (const std::exception& exp) { ++unhappy_counter; }
try { expect_true(false); }
catch (const std::exception& exp) { ++unhappy_counter; }
try { expect_true(0); } // NOLINT
catch (const std::exception& exp) { ++unhappy_counter; }
// clang-format on
};
Case { "expect_false - happy" } = [] {
auto *oongaboonga_is_slacking = (int *)nullptr;
expect_false(oongaboonga_is_slacking);
expect_false(false);
expect_false(0); // NOLINT
};
Case { "expect_false - unhappy" } = [] {
auto oongaboonga = int {};
auto *oonga_oonga_can_rest_now = (int *)nullptr;
auto unhappy_counter = 0u;
// clang-format off
try { expect_false(oonga_oonga_can_rest_now); }
catch (const std::exception& exp) { ++unhappy_counter; }
try { expect_false(true); }
catch (const std::exception& exp) { ++unhappy_counter; }
try { expect_false(!false); }
catch (const std::exception& exp) { ++unhappy_counter; }
try { expect_false(1); } // NOLINT
catch (const std::exception& exp) { ++unhappy_counter; }
// clang-format on
};
Case { "expect_true - unhappy" } = [] {
auto unhappy_counter = 0u;
auto *where_oongaboonga_ptr = (int *)nullptr;
// clang-format off
try { expect_true(where_oongaboonga_ptr); }
catch (const std::exception& exp) { ++unhappy_counter; }
try { expect_true(!true); }
catch (const std::exception& exp) { ++unhappy_counter; }
try { expect_true(false); }
catch (const std::exception& exp) { ++unhappy_counter; }
try { expect_true(0); } // NOLINT
catch (const std::exception& exp) { ++unhappy_counter; }
// clang-format on
};
Case { "expect_eq - happy" } = [] {
expect_eq(5, 5);
expect_eq(20.0, 20.0);
expect_eq(true, 1);
};
Case { "expect_eq - unhappy" } = [] {
auto unhappy = false;
// clang-format off
try { expect_eq(true, false); }
catch (const std::exception &exp) { unhappy = true; }
// clang-format on
if (!unhappy)
{
throw std::runtime_error { "expect_eq unhappy" };
}
};
Case { "expect_ne - happy " } = [] {
expect_ne(5, 5.0000001);
expect_ne(20.0, 69.0);
expect_ne(true, 0);
};
Case { "expect_ne - unhappy" } = [] {
auto unhappy_counter = 0u;
// clang-format off
try { expect_ne(5, 5); }
catch (const std::exception &exp) { ++unhappy_counter; }
try { expect_ne(20.0, 20.0); }
catch (const std::exception &exp) { ++unhappy_counter; }
try { expect_ne(true, 1); }
catch (const std::exception &exp) { ++unhappy_counter; }
// clang-format on
if (unhappy_counter != 3)
{
throw std::runtime_error { "expect_ne unhappy" };
}
};
Case { "expect_throw - happy" } = [] {
expect_throw([] { throw std::runtime_error { "nonsense" }; });
};
Case { "expect_throw - unhappy" } = [] {
auto unhappy = false;
// clang-format off
try { expect_throw([] {}); }
catch (const std::exception &exp) { unhappy = true; }
// clang-format on
if (!unhappy)
{
throw std::runtime_error { "expect_throw - unhappy" };
}
};
Case { "expect_le - happy" } = [] {
expect_le(69, 420);
expect_le(19.694206942069420, 20.0);
expect_le(false, 1);
};
Case { "expect_le - unhappy" } = [] {
auto unhappy_counter = 0u;
// clang-format off
try { expect_le(20020619 + 23, 20020619 ); }
catch (const std::exception &exp) { ++unhappy_counter; }
try { expect_le(420, 69); }
catch (const std::exception &exp) { ++unhappy_counter; }
try { expect_le(20.0, 19.694206942069420); }
catch (const std::exception &exp) { ++unhappy_counter; }
try { expect_le(1, false); }
catch (const std::exception &exp) { ++unhappy_counter; }
// clang-format on
if (unhappy_counter != 4)
{
throw std::runtime_error { "expect_le - unhappy" };
}
};
};

View file

@ -1,36 +0,0 @@
#include <test/test.hpp>
lt::test::Suite meta = []() {
using lt::test::expect_eq;
using lt::test::expect_true;
lt::test::Case { "test_1" } = [] {
expect_eq(5, 5);
};
lt::test::Case { "test_2" } = [] {
expect_eq(20.0, 20.0);
};
lt::test::Case { "test_3" } = [] {
auto exception_thrown = false;
try
{
expect_eq(true, false);
}
catch (const std::exception &exp)
{
exception_thrown = true;
}
expect_true(exception_thrown);
};
lt::test::Case { "test_4" } = [] {
expect_eq(true, 1);
};
lt::test::Case { "test_5" } = [] {
};
};

View file

@ -7,20 +7,78 @@
namespace lt::test {
template<typename T>
concept Printable = requires(std::ostream &os, T t) {
{ os << t } -> std::same_as<std::ostream &>;
concept Printable = requires(std::ostream &stream, T value) {
{ stream << value } -> std::same_as<std::ostream &>;
} || requires(std::ostream &stream, T value) {
{ stream << std::to_underlying<T>(value) } -> std::same_as<std::ostream &>;
};
template<typename T>
concept Testable = Printable<T> && std::equality_comparable<T>;
constexpr void expect_unreachable(
std::source_location source_location = std::source_location::current()
)
{
throw std::runtime_error {
std::format(
"Failed unreachable expectation:\n"
"\tlocation: {}:{}",
source_location.file_name(),
source_location.line()
),
};
};
constexpr void expect_throw(
std::invocable auto invocable,
std::source_location source_location = std::source_location::current()
)
{
try
{
invocable();
}
catch (const std::exception &exp)
{
return;
}
throw std::runtime_error {
std::format(
"Failed throwing expectation:\n"
"\tlocation: {}:{}",
source_location.file_name(),
source_location.line()
),
};
}
constexpr void expect_eq(
Testable auto lhs,
Testable auto rhs,
std::source_location source_location = std::source_location::current()
)
{
if (lhs != rhs)
if constexpr (std::is_enum_v<decltype(lhs)>)
{
if (lhs != rhs)
{
throw std::runtime_error {
std::format(
"Failed equality expectation:\n"
"\tactual: {}\n"
"\texpected: {}\n"
"\tlocation: {}:{}",
std::to_underlying<decltype(lhs)>(lhs),
std::to_underlying<decltype(rhs)>(rhs),
source_location.file_name(),
source_location.line()
),
};
}
}
else if (lhs != rhs)
{
throw std::runtime_error {
std::format(
@ -102,6 +160,27 @@ constexpr void expect_false(
}
}
constexpr void expect_not_nullptr(
auto *pointer,
std::source_location source_location = std::source_location::current()
)
{
if (pointer == nullptr)
{
throw std::runtime_error {
std::format(
"Failed true expectation:\n"
"\tactual: nullptr\n"
"\texpected: not nullptr\n"
"\tlocation: {}:{}",
source_location.file_name(),
source_location.line()
),
};
}
}
constexpr void expect_le(
Testable auto lhs,
Testable auto rhs,

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