diff --git a/.clang-format b/.clang-format new file mode 100644 index 00000000..c24ad7d9 --- /dev/null +++ b/.clang-format @@ -0,0 +1,25 @@ +UseTab: Never +IndentWidth: 4 +BreakBeforeBraces: Allman +AllowShortIfStatementsOnASingleLine: false +IndentCaseLabels: false +ColumnLimit: 0 +NamespaceIndentation: None +FixNamespaceComments: false +AllowAllConstructorInitializersOnNextLine: true +BreakConstructorInitializers: BeforeComma +AllowShortBlocksOnASingleLine: Never +AlignTrailingComments: true +ColumnLimit: 120 +AllowShortFunctionsOnASingleLine: None +BinPackArguments: false +BinPackParameters: false +AlignAfterOpenBracket: AlwaysBreak +IndentCaseLabels: true +AllowAllParametersOfDeclarationOnNextLine: false +PenaltyBreakBeforeFirstCallParameter: 80 +PenaltyReturnTypeOnItsOwnLine: 1000 +ExperimentalAutoDetectBinPacking: false +PointerAlignment: Right +AlwaysBreakTemplateDeclarations: Yes +AllowShortCaseLabelsOnASingleLine: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..3a01b67f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,42 @@ +name: build + +on: [pull_request] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest, windows-latest] + build_type: [Debug, Release] + + steps: + - uses: actions/checkout@v2 + + - name: Create Build Environment + # Some projects don't allow in-source building, so create a separate build directory + # We'll use this as our working directory for all subsequent commands + run: cmake -E make_directory ${{github.workspace}}/build + + - name: Configure CMake + # Use a bash shell so we can use the same syntax for environment variable + # access regardless of the host operating system + shell: bash + working-directory: ${{github.workspace}}/build + # Note the current convention is to use the -S and -B options here to specify source + # and build directories, but this is only available with CMake 3.13 and higher. + # The CMake binaries on the Github Actions machines are (as of this writing) 3.12 + run: cmake $GITHUB_WORKSPACE + + - name: Build + working-directory: ${{github.workspace}}/build + shell: bash + # Execute the build. You can specify a specific target with "--target " + run: cmake --build . --config ${{ matrix.build_type }} + + - name: Test + working-directory: ${{github.workspace}}/build + shell: bash + # Execute tests defined by the CMake configuration. + # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail + run: ctest \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..b29813be --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,69 @@ +name: release + +on: + create: + +jobs: + build: + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + runs-on: ${{ matrix.config.os }} + strategy: + matrix: + config: + - { + os: macos-latest, + package_ext: .tar.gz + } + - { + os: windows-latest, + package_ext: .zip + } + build_type: [Release] + + steps: + - uses: actions/checkout@v2 + + - name: Create Build Environment + # Some projects don't allow in-source building, so create a separate build directory + # We'll use this as our working directory for all subsequent commands + run: cmake -E make_directory ${{github.workspace}}/build + + - name: Configure CMake + # Use a bash shell so we can use the same syntax for environment variable + # access regardless of the host operating system + shell: bash + working-directory: ${{github.workspace}}/build + # Note the current convention is to use the -S and -B options here to specify source + # and build directories, but this is only available with CMake 3.13 and higher. + # The CMake binaries on the Github Actions machines are (as of this writing) 3.12 + run: cmake $GITHUB_WORKSPACE + + - name: Build + working-directory: ${{github.workspace}}/build + shell: bash + # Execute the build. You can specify a specific target with "--target " + run: cmake --build . --config ${{ matrix.build_type }} + + - name: Test + working-directory: ${{github.workspace}}/build + shell: bash + # Execute tests defined by the CMake configuration. + # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail + run: ctest + + - name: Package + working-directory: ${{github.workspace}}/build + shell: bash + run: cpack -B package + + - uses: actions/checkout@v2 + working-directory: ${{github.workspace}}/build + with: + name: upload + path: package/.*${{ matrix.config.package_ext }} + + - uses: actions/download-artifact@v2 + working-directory: ${{github.workspace}}/build + with: + name: release_build + path: package/.*${{ matrix.config.package_ext }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7f0900ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +*.DS_STORE +.ycm_extra_conf.* +CMakeCache.txt +CMakeFiles/ +CMakeScripts/ +*.cmake +build/ +xcode_build/ +*.swp +obj/ +*.a +*.o +*.data +tags +tmp/ +Makefile +.clangd/ +compile_commands.json +.vscode/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..4153a740 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,118 @@ +cmake_minimum_required(VERSION 3.18) + +project( + iris + VERSION "1.0.0" + DESCRIPTION "Cross-platform game engine" + LANGUAGES C CXX) + +include(CMakePackageConfigHelpers) +include(FetchContent) +include(GNUInstallDirs) +include(GenerateExportHeader) + +# set options for library +option(IRIS_BUILD_UNIT_TESTS "whether to build unit tests" ON) + +set(CMAKE_CXX_STANDARD 20) +set(ASM_OPTIONS "-x assembler-with-cpp") + +# if a platform wasn't supplied then default to current platform +if(NOT IRIS_PLATFORM) + if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") + set(IRIS_PLATFORM "MACOS") + set(IRIS_ARCH "X86_64") + elseif(${CMAKE_SYSTEM_NAME} MATCHES "Windows") + set(IRIS_PLATFORM "WIN32") + set(IRIS_ARCH "X86_64") + else() + message(FATAL_ERROR "Unsupported platform: ${CMAKE_SYSTEM_NAME}") + endif() +endif() + +if(IRIS_PLATFORM MATCHES "MACOS" OR IRIS_PLATFORM MATCHES "IOS") + enable_language(OBJC) + enable_language(OBJCXX) + enable_language(ASM) +endif() + +# set options for third party libraries +set(BUILD_UNIT_TESTS OFF CACHE BOOL "" FORCE) +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(BUILD_CPU_DEMOS OFF CACHE BOOL "" FORCE) +set(BUILD_BULLET2_DEMOS OFF CACHE BOOL "" FORCE) +set(BUILD_EXTRAS OFF CACHE BOOL "" FORCE) +set(BUILD_DOCS OFF CACHE BOOL "" FORCE) +set(ASSIMP_BUILD_ASSIMP_TOOLS OFF CACHE BOOL "" FORCE) +set(ASSIMP_BUILD_SAMPLES OFF CACHE BOOL "" FORCE) +set(ASSIMP_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(ASSIMP_BUILD_ZLIB ON CACHE BOOL "" FORCE) +set(ASSIMP_BUILD_ALL_IMPORTERS_BY_DEFAULT OFF CACHE BOOL "" FORCE) +set(ASSIMP_BUILD_ALL_EXPORTERS_BY_DEFAULT OFF CACHE BOOL "" FORCE) +set(ASSIMP_BUILD_FBX_IMPORTER ON CACHE BOOL "" FORCE) +set(ASSIMP_NO_EXPORT ON CACHE BOOL "" FORCE) + +# fetch third party libraries +# note that in most cases we manually populate and add, this alloes us to use +# EXCLUDE_FROM_ALL to prevent them from being included in the install step + +FetchContent_Declare( + assimp + GIT_REPOSITORY https://github.com/assimp/assimp + GIT_TAG v5.0.1) +FetchContent_GetProperties(assimp) +if(NOT assimp_POPULATED) + FetchContent_Populate(assimp) + add_subdirectory(${assimp_SOURCE_DIR} ${assimp_BINARY_DIR} EXCLUDE_FROM_ALL) +endif() + +FetchContent_Declare( + bullet + GIT_REPOSITORY https://github.com/bulletphysics/bullet3 + GIT_TAG 3.17) +FetchContent_GetProperties(bullet) +if(NOT bullet_POPULATED) + FetchContent_Populate(bullet) + add_subdirectory(${bullet_SOURCE_DIR} ${bullet_BINARY_DIR} EXCLUDE_FROM_ALL) +endif() + +# stb doesn't have a cmake file, so just make it available +FetchContent_Declare( + stb + GIT_REPOSITORY https://github.com/nothings/stb + GIT_TAG c0c982601f40183e74d84a61237e968dca08380e + CONFIGURE_COMMAND "" BUILD_COMMAND "") +FetchContent_MakeAvailable(stb) + +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG release-1.11.0) +FetchContent_GetProperties(googletest) +if(NOT googletest_POPULATED) + FetchContent_Populate(googletest) + add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR} EXCLUDE_FROM_ALL) +endif() + +if(IRIS_PLATFORM MATCHES "WIN32") + FetchContent_Declare( + directx-headers + GIT_REPOSITORY https://github.com/microsoft/DirectX-Headers.git + GIT_TAG v1.4.9) + FetchContent_GetProperties(directx-headers) + if(NOT directx-headers_POPULATED) + FetchContent_Populate(directx-headers) + add_subdirectory(${directx-headers_SOURCE_DIR} ${directx-headers_BINARY_DIR} EXCLUDE_FROM_ALL) + endif() +endif() + +add_subdirectory("src") +add_subdirectory("samples") + +if(IRIS_BUILD_UNIT_TESTS) + enable_testing() + include(CTest) + add_subdirectory("tests") +endif() + +include(cmake/cpack.cmake) diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..36b7cd93 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..9d96abc3 --- /dev/null +++ b/README.md @@ -0,0 +1,544 @@ +

+ +

+ +# IRIS +Iris is a cross-platform game engine written in modern C++ + +[![build](https://github.com/irisengine/iris/actions/workflows/build.yml/badge.svg)](https://github.com/irisengine/iris/actions/workflows/build.yml) ![C++20](https://img.shields.io/badge/-20-f34b7d?logo=cplusplus) [![License](https://img.shields.io/badge/License-Boost%201.0-lightblue.svg)](https://www.boost.org/LICENSE_1_0.txt) ![Platforms](https://img.shields.io/badge/platforms-windows%20%7C%20macos%20%7C%20ios-lightgrey) + +# Table of Contents +1. [Screenshots](#screenshots) +2. [Features](#features) +3. [Dependencies](#dependencies) +4. [Included third-party libraries](#included-third-party-libraries) +5. [Using iris](#using-iris) + 1. [Prebuilt libraries](#prebuilt-libraries) + 2. [System install](#system-install) + 3. [Build in project](#build-in-project) +6. [Building](#building) + 1. [Options](#options) + 2. [Command line](#command-line) + 3. [Visual Studio Code / Visual Studio](#visual-studio-code-visual-studio) + 5. [Xcode](#xcode) +7. [Examples](#examples) +8. [Design](#design) + 1. [Versioning](#versioning) + 2. [Compile/Runtime choices](#compileruntime-choices) + 3. [Managers](#managers) + 4. [Memory management](#memory-management) + 5. [core](#core) + 6. [events](#events) + 7. [graphics](#graphics) + 1. [render_graph](#render_graph) + 8. [jobs](#jobs) + 9. [log](#log) + 10. [networking](#networking) + 11. [physics](#physics) + +--- + +## Screenshots +![zombie](media/zombie.png) +![physics](media/physics.png) + +## Features +* Cross platform: Windows, macOS and iOS +* Multiple rendering backends: D3D12, Metal, OpenGL +* 3D rendering and physics +* HDR +* Graph based shader compiler +* Skeleton animation +* Job based paralelism (fiber and thread implementations) +* Networking + +## Dependencies +The following dependencies are required to build iris: +1. cmake > 3.18 +2. C++20 compiler + +The following compilers have been tested + +| Compiler | Version | Platform | +| -------- | ------- | -------- | +| clang | 12.0.1 | macOS | +| clang | 11.0.0 | macOS | +| Apple clang | 12.0.0 | macOS | +| Apple clang | 12.0.0 | iOS | +| msvc | 19.29.30133.0 | windows | + +## Included third-party libraries +The following dependencies are automatically checked out as part of the build: + +| Dependency | Version | License | +| ---------- | ------- | ------- | +| [assimp](https://github.com/assimp/assimp) | [5.0.1](https://github.com/assimp/assimp/releases/tag/v5.0.1) | [![License](https://img.shields.io/badge/License-assimp-lightblue.svg)](https://github.com/assimp/assimp/blob/master/LICENSE)| +| [bullet](https://github.com/bulletphysics/bullet3) | [3.17](https://github.com/bulletphysics/bullet3/releases/tag/3.17) | [![License: Zlib](https://img.shields.io/badge/License-Zlib-lightblue.svg)](https://opensource.org/licenses/Zlib) | +| [stb](https://github.com/nothings/stb) | [c0c9826](https://github.com/nothings/stb/tree/c0c982601f40183e74d84a61237e968dca08380e) | [![License: MIT](https://img.shields.io/badge/License-MIT-lightblue.svg)](https://opensource.org/licenses/MIT) / [![License: Unlicense](https://img.shields.io/badge/License-Unlicense-lightblue.svg)](http://unlicense.org/)| +| [googletest](https://github.com/google/googletest.git) | [1.11.0](https://github.com/google/googletest/releases/tag/release-1.11.0) | [![License](https://img.shields.io/badge/License-BSD%203--Clause-lightblue.svg)](https://opensource.org/licenses/BSD-3-Clause) | +| [directx-headers](https://github.com/microsoft/DirectX-Headers.git) | [1.4.9](https://github.com/microsoft/DirectX-Headers/releases/tag/v1.4.9) | [![License: MIT](https://img.shields.io/badge/License-MIT-lightblue.svg)](https://opensource.org/licenses/MIT) | + + +## Using iris +Iris (and all of its dependencies) are built as static libraries. There are three ways you can include iris in your project. These all assume you are using cmake, it is theoretically possible to integrate iris into other build systems but that is beyond the scope of this document. + +### Prebuilt libraries +Prebuilt binaries (and required headers) are available in [releases](https://github.com/irisengine/iris/releases). Simply download, extract and copy somewhere. They can be either checked into your project or stored externally. Then simply add the following to your project cmake: +```cmake +find_package(iris REQUIRED PATHS path/to/iris/lib/cmake) +target_link_libraries(my_project iris::iris) +``` + +### System install +After [building](#command-line) run the following as root/admin from the build directory to install iris into your system: +```cmake +cmake --install . +``` + +Then simply add the following to your project: +```cmake +find_package(iris REQUIRED) +target_link_libraries(my_project iris::iris) +``` + +### Build in project +It is also possible to build iris as part of your project. Add the source to your project (either by copying the files or as a git submodule). Then add the following to your project: +```cmake +add_subdirectory(iris) +target_include_directories(my_project PRIVATE iris/include) +target_link_libraries(my_project iris::iris) +``` + +Alternatively you can let cmake handle the checking out of iris: +```cmake +FetchContent_Declare( + iris + GIT_REPOSITORY https://github.com/irisengine/iris + GIT_TAG v1.0.0) +FetchContent_GetProperties(iris) +if(NOT iris_POPULATED) + FetchContent_Populate(iris) + add_subdirectory(${iris_SOURCE_DIR} ${iris_BINARY_DIR} EXCLUDE_FROM_ALL) +endif() + +target_include_directories(my_project PRIVATE iris/include) +target_link_libraries(my_project iris::iris) +``` + +## Building + +### Options +| Cmake option | Default value | +| ------------ | ------------- | +| IRIS_BUILD_UNIT_TESTS | ON | + +The following build methods are supported + +### Command line +The following commands will build a debug version of iris. Note that this also works in PowerShell + +```bash +mkdir build +cd build +cmake .. +cmake --build . + +# to run tests +ctest +``` + +### Visual Studio Code / Visual Studio +Opening the root [`CMakeLists.txt`](/CMakeLists.txt) file in either tool should be sufficient. For vscode you will then have to select an appropriate kit. On Windows you will need to ensure the "Desktop development with C++" workload is installed. + +Tests can be run with a googletest adaptor e.g. [visual studio](https://docs.microsoft.com/en-us/visualstudio/test/how-to-use-google-test-for-cpp?view=vs-2019) or [vscode](https://marketplace.visualstudio.com/items?itemName=DavidSchuldenfrei.gtest-adapter) + +### Xcode +You will need to generate the Xcode project files. + +For macOS: +```bash +mkdir build +cd build +cmake -GXcode .. +``` + +For iOS: +```bash +mkdir build +cd build +cmake .. -G Xcode -DCMAKE_TOOLCHAIN_FILE=../toolchains/ios.toolchain.cmake -DDEPLOYMENT_TARGET=14.3 +``` + +## Examples + +The samples directory contains some basic usages. +* [sample_browser](/samples/sample_browser) - single executable with multiple graphics samples (tab to cycle through them) +* [jobs](/samples/jobs) - a quick and dirty path tracer to showcase and test the jobs system +* [networking](/samples/networking) - a client and server applications showcasing networking, client side prediction and lag compensation + + +Some additional snippets are included below. + +**Create a window** +```c++ +#include "iris/core/root.h" +#include "iris/events/event.h" +#include "iris/graphics/window.h" +#include "iris/graphics/window_manager.h" + +void go(int, char **) +{ + auto *window = iris::Root::window_manager().create_window(800, 800); + auto running = true; + + do + { + auto event = window->pump_event(); + while (event) + { + if (event->is_key(iris::Key::ESCAPE)) + { + running = false; + break; + } + + event = window->pump_event(); + } + + window->render(); + } while (running); +} + +int main(int argc, char **argv) +{ + iris::start(argc, argv, go); +} +``` + +*All further snippets will assume be in `go` and will omit headers for brevity* + +**Render a red cube** +```c++ + auto *window = iris::Root::window_manager().create_window(800, 800); + auto running = true; + + iris::Scene scene; + scene.create_entity( + nullptr, + iris::Root::mesh_manager().cube({1.0f, 0.0f, 0.0f}), + iris::Transform{{0.0f, 0.0f, 0.0f}, {}, {10.0f, 10.0f, 10.0f}}); + + iris::Camera camera{iris::CameraType::PERSPECTIVE, 800, 800}; + + window->set_render_passes({{&scene, &camera, nullptr}}); + + do + { + auto event = window->pump_event(); + while (event) + { + if (event->is_key(iris::Key::ESCAPE)) + { + running = false; + break; + } + + event = window->pump_event(); + } + + window->render(); + } while (running); +``` + +## Design + +### Versioning +The public API of iris is versioned using [semver](https://semver.org/). This means that when upgrading you can expect the following outcomes: +* Major version -> your project could no longer compile/link +* Minor version -> your project may not function the same as before +* Patch version -> your project should function the same, if you were not relying on the broken behavior. + +The internal API could change frequently and should not be used. As a rule of thumb the public API is defined in any header file in the top-level folders in `inlcude/iris` and any subfolders are internal. + +### Compile/Runtime choices +Iris provides the user with several runtime choices e.g. rendering backend and physics engine. These are all runtime decisions (see [Managers](#managers)) and implemented via classic class inheritance. Some choices don't make sense to make at runtime e.g. `Semaphore` will be implemented with platform specific primitives so there is no runtime choice to make. To remove the overheard of inheritance and make this a simple compile time choice we define a single header ([semaphore.h](/include/iris/core/semaphore.h)) with the API and provide several different implementations ([macos](/src/core/macos/semaphore.cpp), [windows](/src/core/win32/semaphore.cpp)). Cmake can then pick the appropriate one when building. We use the [pimpl](https://en.cppreference.com/w/cpp/language/pimpl) idiom to keep implementation details out of the header. + +### Managers +In order to easily facilitate the runtime selection of components iris makes use of several manager classes. A manager class can be thought of as a factory class with state. Managers are registered in [`Root`](/include/iris/core/root.h) and then accessed via the [`Root`](/include/iris/core/root.h) API. [`start()`](/include/iris/core/start.h) registers all builtin components for a given platform and sets sensible defaults. It may seem like a lot of machinery to have to registers managers, access them via [`Root`](/include/iris/core/root.h) then use those to actually create the objects you want, but the advantage is a complete decoupling of the implementation from [`Root`](/include/iris/core/root.h). It is therefore possible to provide your own implementations of these components, register, then use them. + +### Memory management +Iris manages the memory and lifetime of primitives for the user. If the engine is creating an object and returns a pointer it can be assumed that the pointer is not null and will remain valid until explicitly returned to the engine by the user. + +### [`core`](/include/iris/core) +The directory contains primitives used throughout the engine. Details on some key parts are defined below. + +#### Root +The [`Root`](/include/iris/core/root.h) provides singleton access to various core parts of iris, including the registered [managers](#managers). + +#### Start +The [`start`](/include/iris/core/start.h) function allows iris to perform all engine start up and tear down before handing over to a user supplied function. All iris functions are undefined if called outside the provided callback. + +#### Error handling +In iris errors are handled one of two ways, depending on the nature of the error: +1. Invariants that must hold but are not recoverable - in this case `expect` is used and `std::abort` is called on failure. This is analogous to an assert and thy are stripped in release. Example: failing to allocate a graphics api specific buffer. +2. Invariants that must hold but are recoverable - in this case `ensure` is used and an exception is thrown on failure. This allows someone further up the stack to catch and recover. Example: loading a texture from a missing file. + +It's not always clear cut when which should be used, the main goal is that all potential errors are handled in some way. See [error_handling.h](/include/iris/core/error_handling.h) for `expect` and `ensure` documentation. + +### [`events`](/include/iris/events) +These are user input events e.g. key press, screen touch. They are captured by a `Window` and can be pumped and then processed. Note that every tick all available events should be pumped. + +```c++ +auto event = window->pump_event(); +while (event) +{ + // handle event here + + event = window->pump_event(); +} +``` + +### [`graphics`](/include/iris/graphics) +All rendering logic is encapsulated in graphics. API agnostic interfaces are defined and implementations can be selected at runtime. + +A rough breakdown of the graphics design is below. The public interface is what users should use. The arrow is (mostly) used to denote ownership. + +```text + + + +--------------+ +---------------------+ + .--->| GraphicsMesh |--->| Graphics primitives | + | +--------------+ +---------------------+ + | + | +----------------+ +------------------+ + .--->| ShaderCompiler |--->| GraphicsMaterial | + | +----------------+ +------------------+ + | + | +--------------------+ + .--->| RenderQueueBuilder | + | +--------------------+ + | + | + +----------+ +------------------+ + | OSWindow | | GraphicsRenderer | + +----------+ +------------------+ + | | + | | +private interface | | +~~~~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +public interface | | + | | + | | ++---------------+ +--------+ +----------+ +----------+ +| WindowManager |--->| Window |--->| Renderer |---->| render() | ++---------------+ +--------+ +----------+ +----------+ + | + | +---------------------+ +-------+ + '-->| set_current_scene() |<---| Scene | + +---------------------+ +-------+ + | +--------------+ + '--->| RenderEntity | + | +--------------+ + | | +------+ + | '--->| Mesh | + | | +------+ + | | +----------+ + | '--->| Skeleton | + | +----------+ + | +-------------+ + '--->| RenderGraph | + | +-------------+ + | +-------+ + '--->| Light | + +-------+ + +``` + +#### [`render_graph`](/include/iris/graphics/render_graph) +The render graph allows a user to define the effect of shaders in a shader language agnostic way. This is then compiled into the appropriate shader code (GLSL, HLSL, MSL) for the current window. [`Scene`](/include/iris/graphics/scene.h) creates and owns a [`RenderGraph`](/include/iris/graphics/render_graph/render_graph.h) which can be used for any [`RenderEntity`](/include/iris/graphics/render_entity.h) in that [`Scene`](/include/iris/graphics/scene.h) (it is undefined to use [`RenderGraph`](/include/iris/graphics/render_graph/render_graph.h) object in a [`Scene`](/include/iris/graphics/scene.h) that did not create it). + +A [`RenderGraph`](/include/iris/graphics/render_graph/render_graph.h) owns a [`RenderNode`](/include/iris/graphics/render_graph/render_node.h) which is the root of the graph. To configure the output of the shader the inputs of the [`RenderNode`](/include/iris/graphics/render_graph/render_node.h) should be set. An example: + +**Basic bloom** +```text + Render +Main scene ~~~~~~~~ -----. + | +.------------------------' +| +| +---------------------+ +|---------------. | Arithmetic Node | +----------------------+ +--------------+ +| | |=====================| | Conditional Node | | Render Node |~~~~~~ -. +| +-----------+ '->O value1 | |======================| |==============| | +| | Threshold | -->O value2 |----->O input_value1 |->O colour_input | | +| +-----------+ .->O arithmetic operator | .--->O input_value2 | +--------------| | +| | +---------------------+ |.-->O output_value1 | | +| +-----+ | ||.->O output_value2 | | +| | DOT |-------' +------+ |||.>O conditional_operator | | +| +-----+ | 1.0f |----------------'||| +----------------------+ | +| +------+ ||| | +|-------------------------------------------'|| | +| +-------------+ || | +| | Zero colour |-----------'| | +| +-------------+ | | +| | | +| +---------+ | | +| | GREATER |----------------' | +| +---------+ | +| | +| .-----------------------------------------------------------------------------------------------' +| | +| | +| | +------------+ +--------------+ +| | | Blur Node | | Render Node | +| | |============| |==============| ~~~~~ --. +| '--->O input_node |---->O colour_input | | +| +------------+ +--------------+ | +| | +| .--------------------------------------' +| | +| | +---------------------+ +| | | Arithmetic Node | +--------------+ +| | |=====================| | Render Node | +| '-->O value1 | |==============|~~~~~~~~~~> Screen +'-------------->O value2 |------->O colour_input | + .-->O arithmetic operator | +--------------+ + +-----+ | +---------------------+ + | ADD |----' + +-----+ +``` + +Which in code would look something like: +```c++ +iris::Scene main_scene; +auto *main_rt = window->create_render_target(); +main_scene.create_entity( + nullptr, + iris::Root::mesh_manager().cube({100.0f, 0.0f, 0.0f}), + iris::Transform{{0.0f, 0.0f, -10.0f}, {}, {10.0f, 10.0f, 10.0f}}); + +iris::Scene bright_pass_scene; +auto *bright_pass_rt = window->create_render_target(); +auto *bright_pass_rg = bright_pass_scene.create_render_graph(); +bright_pass_rg->render_node()->set_colour_input(bright_pass_rg->create( + bright_pass_rg->create( + bright_pass_rg->create(main_rt->colour_texture()), + bright_pass_rg->create>(iris::Colour{0.2126f, 0.7152f, 0.0722f, 0.0f}), + iris::ArithmeticOperator::DOT), + bright_pass_rg->create>(1.0f), + bright_pass_rg->create(main_rt->colour_texture()), + bright_pass_rg->create>(iris::Colour{0.0f, 0.0f, 0.0f, 1.0f}), + iris::ConditionalOperator::GREATER)); + +bright_pass_scene.create_entity( + bright_pass_rg, + iris::Root::mesh_manager().sprite({1.0f, 1.0f, 1.0f}), + iris::Transform{{0.0f, 0.0f, 0.0f}, {}, {800.0f, 800.0f, 1.0f}}); + +iris::Scene blur_pass_scene; +auto *blur_pass_rt = window->create_render_target(); +auto *blur_pass_rg = blur_pass_scene.create_render_graph(); +blur_pass_rg->render_node()->set_colour_input(blur_pass_rg->create( + blur_pass_rg->create(bright_pass_rt->colour_texture()))); + +blur_pass_scene.create_entity( + blur_pass_rg, + iris::Root::mesh_manager().sprite({1.0f, 1.0f, 1.0f}), + iris::Transform{{0.0f, 0.0f, 0.0f}, {}, {800.0f, 800.0f, 1.0f}}); + +iris::Scene final_scene; +auto *final_rg = final_scene.create_render_graph(); +final_rg->render_node()->set_colour_input(final_rg->create( + final_rg->create(blur_pass_rt->colour_texture()), + final_rg->create(main_rt->colour_texture()), + iris::ArithmeticOperator::ADD)); + +final_scene.create_entity( + final_rg, + iris::Root::mesh_manager().sprite({1.0f, 1.0f, 1.0f}), + iris::Transform{{0.0f, 0.0f, 0.0f}, {}, {800.0f, 800.0f, 1.0f}}); + +iris::Camera persective_camera{iris::CameraType::PERSPECTIVE, 800, 800}; +iris::Camera orth_camera{iris::CameraType::ORTHOGRAPHIC, 800, 800}; + +window->set_render_passes( + {{&main_scene, &persective_camera, main_rt}, + {&bright_pass_scene, &orth_camera, bright_pass_rt}, + {&blur_pass_scene, &orth_camera, blur_pass_rt}, + {&final_scene, &orth_camera, nullptr}}); +``` + +### [`jobs`](/include/iris/jobs) +Iris doesn't use separate threads for each component (e.g. one thread for rendering and another for physics) instead it provides an API for executing independent jobs. This allows for a more scalable approach to parallelism without having to worry about synchronisation between components. + +A [`job`](/include/iris/jobs/job.h) represents a function call and can be a named function or a lambda. + +For simplicity the API for scheduling jobs is exposed via `Root`. There are two options for scheduling: +* `add_jobs()` - fire and forget +* `wait_for_jobs()` - caller waits for all jobs to finish + +Note that a key part of the design is to allow jobs to schedule other jobs with either method. + +Provided in the engine are two implementations of the [`job_system`](/include/iris/jobs/job_system.h): + +**Threads** + +This uses [`std::async`](https://en.cppreference.com/w/cpp/thread/async) to create threads for each job. This is a simple and robust implementation that will work on any supported platform. + +**Fibers** + +There are two problems with the threading implementation: +1. Overheard of OS scheduling threads +2. If a job calls `wait_for_jobs()` it will block, meaning we lose one thread until it is complete + +Fibers attempts to overcome both these issues. A [Fiber](https://en.wikipedia.org/wiki/Fiber_(computer_science)) is a userland execution primitive and yield themselves rather than relying on the OS. When the [FiberJobSystem](/src/jobs/fiber/fiber_job_system.cpp) starts it creates a series of worker threads. When a job is scheduled a Fiber is created for it and placed on a queue, which the worker threads pick up and execute. The key difference between just running on the threads is that if a Fiber calls `wait_for_jobs()` it will suspend and place itself back on the queue thus freeing up that worker thread to work on something else. This means fibers are free to migrate between threads and will not necessarily finish on the thread that started it. + +Fibers are supported on Win32 natively and on Posix iris has an [x86_64](/include/iris/jobs/arch/x86_64/functions.S) [implementation](/src/jobs/fiber/posix/fiber.cpp). They are not currently supported on iOS. + +### [`log`](/include/iris/log) +Iris provides a logging framework, which a user is under no obligation to use. The four log levels are: +1. DEBUG +2. INFO +3. WARN +3. ERROR + +Logging is stripped in release. Internally iris uses an engine specific overload of the logging functions which are disabled by default unless you use `start_debug()` instead if `start()`. + +Logging can be configured to use different outputters and formatters. Currently supported are: +* stdout outputter +* file outputter +* basic text formatter +* ansi terminal colouring formatter +* emoji formatter + +To log use the macros defined in [`log.h`](/include/iris/log/log.h). The format of a log message is tag, message, args. This allows a user to filter out certain tags. +```c++ +LOG_DEBUG("tag", "position: {} health: {}", iris::Vector3{1.0f, 2.0f, 3.0f}, 100.0f); +``` + +### [`networking`](/include/iris/networking) +Networking consists of a series of layered primitives, each one building on the one below and providing additional functionality. A user can use any (or none) of these primitives as they see fit. + +**Socket/ServerSocket** + +[`Socket`](/include/iris/networking/socket.h) and [`ServerSocket`](/include/iris/networking/server_socket.h) are the lowest level primitives and provide an in interface for transferring raw bytes. There are currently two implementations of these interfaces: +* [`UdpSocket`](/include/iris/networking/udp_socket.h) / [`UdpServerSocket`](/include/iris/networking/udp_server_socket.h) - unreliable networking protocol +* [`SimulatedSocket`](/include/iris/networking/simulated_socket.h) / [`SimulatedServerSocket`](/include/iris/networking/simulated_server_socket.h) - a `Socket` adaptor that allows a user to simulate certain networking conditions e.g. packet drop and delay + +**Channels** + +A [`Channel`](/include/iris/networking/channel/channel.h) provides guarantees over an unreliable networking protocol. It doesn't actually do any sending/receiving but buffers [`Packet`](/include/iris/networking/packet.h) objects and only yields them when certain conditions are met. Current channels are: +* [`UnreliableUnorderedChannel`](/include/iris/networking/channel/unreliable_unordered_channel.h) - provides no guarantees +* [`UnreliableSequencedChannel`](/include/iris/networking/channel/unreliable_sequenced_channel.h) - packets are in order, no duplicates but may have gaps +* [`ReliableOrderedChannel`](/include/iris/networking/channel/reliable_ordered_channel.h) - packets are in order, no gaps, no duplicates and guaranteed to arrive + +**ClientConnectionHandler/ServerConnectionHandler** + +[`ClientConnectionHandler`](/include/iris/networking/client_connection_handler.h) and [`ServerConnectionHandler`](/include/iris/networking/server_connection_handler.h) implement a lightweight protocol providing: +* Making a connection +* Handshake +* Clock sync +* Sending/receiving data + +### [`physics`](/inlclude/iris/physics) +Iris comes with bullet physics out the box. The [`physics_system`](/include/iris/physics/physics_system.h) abstract class details the provided functionality. diff --git a/cmake/cpack.cmake b/cmake/cpack.cmake new file mode 100644 index 00000000..82a82c4c --- /dev/null +++ b/cmake/cpack.cmake @@ -0,0 +1,20 @@ +if(IRIS_PLATFORM MATCHES "WINDOWS") + set(CPACK_GENERATOR ZIP) + set(CPACK_SOURCE_GENERATOR ZIP) +else() + set(CPACK_GENERATOR TGZ) + set(CPACK_SOURCE_GENERATOR TGZ) +endif() + +set(CPACK_PACKAGE_VENDOR "iris") +set(CPACK_PACKAGE_CONTACT "") +set(CPACK_RESOURCE_FILE_LICENSE ${PROJECT_SOURCE_DIR}/LICENSE) +set(CPACK_RESOURCE_FILE_README ${PROJECT_SOURCE_DIR}/README.md) +set(CPACK_PACKAGE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +string(TOLOWER ${IRIS_PLATFORM} platform) +string(TOLOWER ${IRIS_ARCH} arch) +string(TOLOWER ${PROJECT_NAME} project) +set(CPACK_SYSTEM_NAME "${platform}-${arch}") + +include(CPack) + diff --git a/cmake/iris-config.cmake.in b/cmake/iris-config.cmake.in new file mode 100644 index 00000000..ba353bcb --- /dev/null +++ b/cmake/iris-config.cmake.in @@ -0,0 +1,5 @@ +@PACKAGE_INIT@ + +if(NOT TARGET iris::iris) + include(${CMAKE_CURRENT_LIST_DIR}/iris-targets.cmake) +endif() \ No newline at end of file diff --git a/include/.gitignore b/include/.gitignore new file mode 100644 index 00000000..774ee175 --- /dev/null +++ b/include/.gitignore @@ -0,0 +1 @@ +iris_version.h \ No newline at end of file diff --git a/include/iris/core/auto_release.h b/include/iris/core/auto_release.h new file mode 100644 index 00000000..fe2a3e96 --- /dev/null +++ b/include/iris/core/auto_release.h @@ -0,0 +1,151 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** + * Generic RAII class for taking ownership of a resource and releasing it when + * this class goes out of scope. + */ +template +class AutoRelease +{ + public: + /** + * Construct a new AutoRelease which doesn't own any object. + */ + AutoRelease() + : AutoRelease(Invalid, nullptr) + { + } + + /** + * Construct a new AutoRelease which takes ownership of a resource + * + * @param resource + * Resource to own + * + * @param deleter + * Function to release resource at end of scope. + */ + AutoRelease(T resource, std::function deleter) + : resource_(resource) + , deleter_(deleter) + { + } + + /** + * Release resource with supplied deleter. + */ + ~AutoRelease() + { + if ((resource_ != Invalid) && deleter_) + { + deleter_(resource_); + } + } + + /** + * Move constructor. Steals ownership from supplied object. + * + * @param other + * Object to move construct from. + */ + AutoRelease(AutoRelease &&other) + : AutoRelease(Invalid, {}) + { + swap(other); + } + + /** + * Move assignment. Steals ownership from supplied object. + * + * @param other + * Object to move assign from. + */ + AutoRelease &operator=(AutoRelease &&other) + { + AutoRelease new_auto_release{std::move(other)}; + swap(new_auto_release); + + return *this; + } + + AutoRelease(const AutoRelease &) = delete; + AutoRelease &operator=(AutoRelease &) = delete; + + /** + * Swap this object with another. + * + * @param other + * Object to swap with. + */ + void swap(AutoRelease &other) + { + std::swap(resource_, other.resource_); + std::swap(deleter_, other.deleter_); + } + + /** + * Get the managed resource. + * + * @returns + * Managed resource. + */ + T get() const + { + return resource_; + } + + /** + * Get the address of the internally managed resource. This is useful if we + * are managing a pointer and need to pass that to another function to get + * set. + * + * @returns + * Address of managed resource. + */ + T *operator&() + { + return std::addressof(resource_); + } + + /** + * Get if this object manages a resource. + * + * @returns + * True if this object managed a resource, false otherwise. + */ + explicit operator bool() const + { + return resource_ != Invalid; + } + + /** + * Cast operator. + * + * @returns + * Managed resource. + */ + operator T() const + { + return resource_; + } + + private: + /** Managed resource. */ + T resource_; + + /** Resource delete function. */ + std::function deleter_; +}; + +} diff --git a/include/iris/core/camera.h b/include/iris/core/camera.h new file mode 100644 index 00000000..bb492ae3 --- /dev/null +++ b/include/iris/core/camera.h @@ -0,0 +1,196 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "core/camera_type.h" +#include "core/matrix4.h" +#include "core/quaternion.h" +#include "core/vector3.h" + +namespace iris +{ + +/** + * Class representing the camera through which a scene is rendered. + */ +class Camera +{ + public: + /** + * Create a new camera, positioned at the origin. + * + * @param + * Type of camera. + * + * @param width + * Width of window. + * + * @param height + * Height of window. + * + * @param depth + * Depth of projection. + */ + Camera(CameraType type, std::uint32_t width, std::uint32_t height, std::uint32_t depth = 1000u); + + /** + * Translate the camera. + * + * @param translate + * Amount to translate. + */ + void translate(const Vector3 &translat); + + /** + * Set the view matrix for the camera. + * + * @param view + * New view matrix. + */ + void set_view(const Matrix4 &view); + + /** + * Get position of camera. + * + * @returns + * Camera position in world space. + */ + Vector3 position() const; + + /** + * Get orientation of camera. + * + * @returns + * Camera orientation. + */ + Quaternion orientation() const; + + /** + * Get direction camera is facing. + * + * @returns + * Camera direction. + */ + Vector3 direction() const; + + /** + * Get the right vector of the camera. + * + * @returns + * Camera right vector. + */ + Vector3 right() const; + + /** + * Get the view matrix4. + * + * @returns + * View matrix4. + */ + Matrix4 view() const; + + /** + * Get the projection matrix4. + * + * @returns + * Projection matrix4. + */ + Matrix4 projection() const; + + /** + * Get camera yaw. + * + * @returns + * Camera yaw. + */ + float yaw() const; + + /** + * Set the yaw of the camera. + * + * @param yaw + * New camera yaw. + */ + void set_yaw(float yaw); + + /** + * Adjust the camera yaw by the supplied value. + * + * @param adjust + * Amount to adjust yaw by. + */ + void adjust_yaw(float adjust); + + /** + * Get camera pitch. + * + * @returns + * Camera pitch. + */ + float pitch() const; + + /** + * Set the pitch of the camera. + * + * @param pitch + * New camera pitch. + */ + void set_pitch(float pitch); + + /** + * Adjust the camera pitch by the supplied value. + * + * @param adjust + * Amount to adjust pitch by. + */ + void adjust_pitch(float adjust); + + /** + * Set world position of camera. + * + * @param position + * New position. + */ + void set_position(const Vector3 &position); + + /** + * Get type of camera. + * + * @returns + * Camera type. + */ + CameraType type() const; + + private: + /** Camera position in world space. */ + Vector3 position_; + + /** Direction camera is facing. */ + Vector3 direction_; + + /** Camera up vector. */ + Vector3 up_; + + /** View Matrix4 for the camera. */ + Matrix4 view_; + + /** Projection Matrix4 for the camera. */ + Matrix4 projection_; + + /** Pitch of camera. */ + float pitch_; + + /** Yaw of camera. */ + float yaw_; + + /** Type of camera. */ + CameraType type_; +}; + +} diff --git a/include/iris/core/camera_type.h b/include/iris/core/camera_type.h new file mode 100644 index 00000000..051e8ad4 --- /dev/null +++ b/include/iris/core/camera_type.h @@ -0,0 +1,23 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** + * Enumeration of camera types. + */ +enum class CameraType : std::uint8_t +{ + PERSPECTIVE, + ORTHOGRAPHIC +}; + +} diff --git a/include/iris/core/colour.h b/include/iris/core/colour.h new file mode 100644 index 00000000..2ecf57b7 --- /dev/null +++ b/include/iris/core/colour.h @@ -0,0 +1,284 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "core/utils.h" + +namespace iris +{ +/** + * A colour with red, green, blue and alpha components. + */ +class Colour +{ + public: + /** + * Create a black colour. + */ + constexpr Colour() + : Colour(0.0f, 0.0f, 0.0f, 1.0f) + { + } + + /** + * Create a new colour with float components. + * + * @param r + * Red. + * + * @param g + * Green. + * + * @param b + * Blue. + * + * @param a + * Alpha. + */ + constexpr Colour(float r, float g, float b, float a = 1.0f) + : r(r) + , g(g) + , b(b) + , a(a) + { + } + + /** + * Create a new colour with integer components. + * + * Ideally this would take the values as std::uint8_t, but this causes an + * ambiguity when integers are passes with the float constructor. + * + * @param r + * Red. + * + * @param g + * Green. + * + * @param b + * Blue. + * + * @param a + * Alpha. + */ + constexpr Colour(std::int32_t r, std::int32_t g, std::int32_t b, std::int32_t a = 255) + : Colour( + static_cast(r) / 255.0f, + static_cast(g) / 255.0f, + static_cast(b) / 255.0f, + static_cast(a) / 255.0f) + { + } + + /** + * Multiply each component by a scalar value. + * + * @param scale + * Scalar value. + * + * @return + * Reference to this Colour. + */ + constexpr Colour &operator*=(float scale) + { + r *= scale; + g *= scale; + b *= scale; + a *= scale; + + return *this; + } + + /** + * Create a new Colour which is this Colour with each component + * multiplied by a scalar value. + * + * @param scale + * scalar value. + * + * @return + * Copy of this Colour with each component multiplied by a + * scalar value. + */ + constexpr Colour operator*(float scale) const + { + return Colour(*this) *= scale; + } + + /** + * Component wise add a Colour to this Colour. + * + * @param colour + * The Colour to add to this. + * + * @return + * Reference to this colour. + */ + constexpr Colour &operator+=(const Colour &colour) + { + r += colour.r; + g += colour.g; + b += colour.b; + a += colour.a; + + return *this; + } + + /** + * Create a new Colour which is this Colour added with a supplied + * Colour. + * + * @param colour + * Colour to add to this. + * + * @return + * Copy of this Colour with each component added to the + * components of the supplied Colour. + */ + constexpr Colour operator+(const Colour &colour) const + { + return Colour(*this) += colour; + } + + /** + * Component wise subtract a Colour to this Colour. + * + * @param v + * The Colour to subtract from this. + * + * @return + * Reference to this Colour. + */ + constexpr Colour &operator-=(const Colour &colour) + { + r -= colour.r; + g -= colour.g; + b -= colour.b; + a -= colour.a; + + return *this; + } + + /** + * Create a new Colour which is this Colour subtracted with a + * supplied Colour. + * + * @param colour + * Colour to subtract from this. + * + * @return + * Copy of this Colour with each component subtracted to the + * components of the supplied Colour. + */ + constexpr Colour operator-(const Colour &colour) const + { + return Colour(*this) -= colour; + } + + /** + * Component wise multiple a Colour to this Colour. + * + * @param colour + * The Colour to multiply. + * + * @returns + * Reference to this Colour. + */ + constexpr Colour &operator*=(const Colour &colour) + { + r *= colour.r; + g *= colour.g; + b *= colour.b; + a *= colour.a; + + return *this; + } + + /** + * Create a new Colour which us this Colour component wise multiplied + * with a supplied Colour. + * + * @param colour + * Colour to multiply with this. + * + * @returns + * Copy of this Colour component wise multiplied with the supplied + * Colour. + */ + constexpr Colour operator*(const Colour &colour) const + { + return Colour{*this} *= colour; + } + + /** + * Check if this colour is equal to another. + * + * @param other + * Colour to compare with. + * + * @returns + * True if both Colours are equal, otherwise false. + */ + bool operator==(const Colour &other) const + { + return compare(r, other.r) && compare(g, other.g) && compare(b, other.b) && compare(a, other.a); + } + + /** + * Check if this colour is not equal to another. + * + * @param other + * Colour to compare with. + * + * @returns + * True if both Colours are unequal, otherwise false. + */ + bool operator!=(const Colour &other) const + { + return !(other == *this); + } + + /** Red. */ + float r; + + /** Green. */ + float g; + + /** Blue. */ + float b; + + /** Alpha. */ + float a; +}; + +/** + * Write a Colour to a stream, useful for debugging. + * + * @param out + * Stream to write to. + * + * @param c + * Colour to write to stream. + * + * @return + * Reference to input stream. + */ +inline std::ostream &operator<<(std::ostream &out, const Colour &c) +{ + out << "r: " << c.r << " " + << "g: " << c.g << " " + << "b: " << c.b << " " + << "a: " << c.a; + + return out; +} + +} diff --git a/include/iris/core/data_buffer.h b/include/iris/core/data_buffer.h new file mode 100644 index 00000000..97612f3a --- /dev/null +++ b/include/iris/core/data_buffer.h @@ -0,0 +1,17 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +namespace iris +{ + +using DataBuffer = std::vector; + +} diff --git a/include/iris/core/error_handling.h b/include/iris/core/error_handling.h new file mode 100644 index 00000000..8f882777 --- /dev/null +++ b/include/iris/core/error_handling.h @@ -0,0 +1,483 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if !defined(__clang__) +#include +#endif + +#if defined(IRIS_PLATFORM_WIN32) +#define WIN32_LEAN_AND_MEAN +#include +#endif + +#include "core/auto_release.h" +#include "core/exception.h" + +namespace iris +{ + +namespace impl +{ + +/** + * Helper function to check an assertion and, if it's false, either log and + * abort or throw an exception. + * + * @param assertion + * The assertion to check. + * + * @param message + * A user supplied message to either log (if abort) or throw with. + * + * @param line + * The source code line where the check was called from. + * + * @param file_name + * The file name where the check was called from. + * + * @param function_name + * The function where the check was called from. + * + * @param drop_mic + * If true then log and abort on assertion failure, else throw exception. + */ +inline void check_and_handle( + bool assertion, + std::string_view message, + int line, + const char *file_name, + const char *function_name, + bool drop_mic) +{ + if (!assertion) + { + std::stringstream strm{}; + + // combine the user supplied message with the source location + strm << message << " -> " << file_name << "(" << line << ") `" << function_name << "`"; + + if (drop_mic) + { + // note we don't use the logging framework here as we don't know + // how it's been configured, safest just to write to stderr + std::cerr << strm.str() << std::endl; + std::abort(); + } + else + { + throw Exception(strm.str()); + } + } +} + +} + +// define a platform specific macro that will break to an attached debugger +// this is useful if we cannot set a breakpoint in an IDE, for example as of +// writing VSCode cannot set a breakpoint in an objc++ (.mm) file +#if defined(IRIS_PLATFORM_WIN32) +#define IRIS_DEBUG_BREAK() \ + do \ + { \ + DebugBreak(); \ + } while (false) +#else +#if defined(IRIS_ARCH_X86_64) +#define IRIS_DEBUG_BREAK() \ + do \ + { \ + asm("int3"); \ + } while (false) +#elif defined(IRIS_ARCH_ARM64) +#define IRIS_DEBUG_BREAK() \ + do \ + { \ + asm("BKPT"); \ + } while (false) +#else +#error unsupported architecture +#endif +#endif + +// here we define expect and ensure functions for error handling +// usage: +// expect - check an unrecoverable invariant holds i.e. something that should always be true +// will call abort on failure and is stripped from release builds +// ensure - check a recoverable invariant holds i.e. something we want to verify at runtime +// will throw an exception on failure and is never stripped +// +// annoyingly (as of writing) clang does not support std::source_location so we +// define two versions, one which uses the standard and another which uses +// clangs builtins +// +// we define the various expect/ensure functions below for different compiles +// and debug/release versions + +#if !defined(NDEBUG) || defined(IRIS_FORCE_EXPECT) + +#if defined(__clang__) +/** + * Check invariant and abort on failure. + * + * @param expectation + * The assertion to check. + * + * @param message + * A user supplied message to log before abort. + */ +inline void expect( + bool expectation, + std::string_view message, + int line = __builtin_LINE(), + const char *file_name = __builtin_FILE(), + const char *function_name = __builtin_FUNCTION()) +{ + impl::check_and_handle(expectation, message, line, file_name, function_name, true); +} + +/** + * Check invariant and abort on failure. This specialisation allows a callback + * to determine if an error occurred. This is useful if errors are detected + * via some global state and need checking via a function e.g. glGetError. + * + * @param check_error + * Callback to determine if an error occurred. message is passed as the + * argument. The return value should be an en empty optional if no error + * occurred or an error message otherwise. + * + * @param message + * A user supplied message to log before abort. + */ +inline void expect( + std::function(std::string_view)> check_error, + std::string_view message, + int line = __builtin_LINE(), + const char *file_name = __builtin_FILE(), + const char *function_name = __builtin_FUNCTION()) +{ + if (const auto final_message = check_error(message); final_message) + { + impl::check_and_handle(false, *final_message, line, file_name, function_name, true); + } +} + +/** + * Check invariant and abort on failure. This specialisation checks if a + * unique_ptr is nullptr. + * + * @param ptr + * unique_ptr to check for nullptr. + * + * @param message + * A user supplied message to log before abort. + */ +template +inline void expect( + const std::unique_ptr &ptr, + std::string_view message, + int line = __builtin_LINE(), + const char *file_name = __builtin_FILE(), + const char *function_name = __builtin_FUNCTION()) +{ + impl::check_and_handle(!!ptr, message, line, file_name, function_name, true); +} + +/** + * Check invariant and abort on failure. This specialisation checks if an + * AutoRelease manages an object. + * + * @param auto_release + * AutoRelease to check. + * + * @param message + * A user supplied message to log before abort. + */ +template +inline void expect( + const AutoRelease &auto_release, + std::string_view message, + int line = __builtin_LINE(), + const char *file_name = __builtin_FILE(), + const char *function_name = __builtin_FUNCTION()) +{ + impl::check_and_handle(!!auto_release, message, line, file_name, function_name, true); +} + +#else + +// see __clang__ defines for documentation + +inline void expect( + bool expectation, + std::string_view message, + std::source_location location = std::source_location::current()) +{ + impl::check_and_handle(expectation, message, location.line(), location.file_name(), location.function_name(), true); +} + +inline void expect( + std::function(std::string_view)> check_error, + std::string_view message, + std::source_location location = std::source_location::current()) +{ + if (const auto final_message = check_error(message); final_message) + { + impl::check_and_handle( + false, *final_message, location.line(), location.file_name(), location.function_name(), true); + } +} + +template +inline void expect( + const std::unique_ptr &ptr, + std::string_view message, + std::source_location location = std::source_location::current()) +{ + impl::check_and_handle(!!ptr, message, location.line(), location.file_name(), location.function_name(), true); +} + +template +inline void expect( + const AutoRelease &auto_release, + std::string_view message, + std::source_location location = std::source_location::current()) +{ + impl::check_and_handle( + !!auto_release, message, location.line(), location.file_name(), location.function_name(), true); +} + +#endif + +#else + +#if defined(__clang__) + +inline void expect( + bool, + std::string_view, + int = __builtin_LINE(), + const char * = __builtin_FILE(), + const char * = __builtin_FUNCTION()) +{ +} + +inline void expect( + std::function(std::string_view)>, + std::string_view, + int = __builtin_LINE(), + const char * = __builtin_FILE(), + const char * = __builtin_FUNCTION()) +{ +} + +template +inline void expect( + const std::unique_ptr &, + std::string_view, + int = __builtin_LINE(), + const char * = __builtin_FILE(), + const char * = __builtin_FUNCTION()) +{ +} + +template +inline void expect( + const AutoRelease &, + std::string_view, + int = __builtin_LINE(), + const char * = __builtin_FILE(), + const char * = __builtin_FUNCTION()) +{ +} + +#else + +inline void expect(bool, std::string_view, std::source_location = std::source_location::current()) +{ +} + +inline void expect( + std::function(std::string_view)>, + std::string_view, + std::source_location = std::source_location::current()) +{ +} + +template +inline void expect(const std::unique_ptr &, std::string_view, std::source_location = std::source_location::current()) +{ +} + +template +inline void expect( + const AutoRelease &, + std::string_view, + std::source_location = std::source_location::current()) +{ +} + +#endif + +#endif + +#if defined(__clang__) + +/** + * Check pre/post-condition and throw on failure. + * + * @param expectation + * The assertion to check. + * + * @param message + * A user supplied message to throw. + */ +inline void ensure( + bool expectation, + std::string_view message, + int line = __builtin_LINE(), + const char *file_name = __builtin_FILE(), + const char *function_name = __builtin_FUNCTION()) +{ + impl::check_and_handle(expectation, message, line, file_name, function_name, false); +} + +/** + * Check pre/post-condition and throw on failure. This specialisation allows a + * callback to determine if an error occurred. This is useful if errors are + * detected via some global state and need checking via a function e.g. + * glGetError. + * + * @param check_error + * Callback to determine if an error occurred. message is passed as the + * argument. The return value should be an en empty optional if no error + * occurred or an error message otherwise. + * + * @param message + * A user supplied message to log before abort. + */ +inline void ensure( + std::function(std::string_view)> check_error, + std::string_view message, + int line = __builtin_LINE(), + const char *file_name = __builtin_FILE(), + const char *function_name = __builtin_FUNCTION()) +{ + if (const auto final_message = check_error(message); final_message) + { + impl::check_and_handle(false, *final_message, line, file_name, function_name, false); + } +} + +/** + * Check pre/post-condition and throw on failure. This specialisation checks if + * a unique_ptr is nullptr. + * + * @param ptr + * unique_ptr to check for nullptr. + * + * @param message + * A user supplied message to log before abort. + */ +template +inline void ensure( + const std::unique_ptr &ptr, + std::string_view message, + int line = __builtin_LINE(), + const char *file_name = __builtin_FILE(), + const char *function_name = __builtin_FUNCTION()) +{ + impl::check_and_handle(!!ptr, message, line, file_name, function_name, false); +} + +/** + * Check pre/post-condition and throw on failure. This specialisation checks if + * an AutoRelease manages an object. + * + * @param auto_release + * AutoRelease to check. + * + * @param message + * A user supplied message to log before abort. + */ +template +inline void ensure( + const AutoRelease &auto_release, + std::string_view message, + int line = __builtin_LINE(), + const char *file_name = __builtin_FILE(), + const char *function_name = __builtin_FUNCTION()) +{ + impl::check_and_handle(!!auto_release, message, line, file_name, function_name, false); +} + +#else + +// see __clang__ defines for documentation + +/** + * Check pre-condition and throw on failure. + * + * @param expectation + * The assertion to check. + * + * @param message + * A user supplied message to throw. + */ +inline void ensure( + bool expectation, + std::string_view message, + std::source_location location = std::source_location::current()) +{ + impl::check_and_handle( + expectation, message, location.line(), location.file_name(), location.function_name(), false); +} + +inline void ensure( + std::function(std::string_view)> check_error, + std::string_view message, + std::source_location location = std::source_location::current()) +{ + if (const auto final_message = check_error(message); final_message) + { + impl::check_and_handle( + false, *final_message, location.line(), location.file_name(), location.function_name(), false); + } +} + +template +inline void ensure( + const std::unique_ptr &ptr, + std::string_view message, + std::source_location location = std::source_location::current()) +{ + impl::check_and_handle(!!ptr, message, location.line(), location.file_name(), location.function_name(), false); +} + +template +inline void ensure( + const AutoRelease &auto_release, + std::string_view message, + std::source_location location = std::source_location::current()) +{ + impl::check_and_handle( + !!auto_release, message, location.line(), location.file_name(), location.function_name(), false); +} + +#endif + +} diff --git a/include/iris/core/exception.h b/include/iris/core/exception.h new file mode 100644 index 00000000..0b3b3be2 --- /dev/null +++ b/include/iris/core/exception.h @@ -0,0 +1,30 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +namespace iris +{ + +/** + * The general exception type for the engine. + */ +class Exception : public std::runtime_error +{ + public: + /** + * Construct a new exception. + * + * @param what + * Exception message. + */ + explicit Exception(const std::string &what); +}; + +} diff --git a/include/iris/core/ios/utility.h b/include/iris/core/ios/utility.h new file mode 100644 index 00000000..8676da64 --- /dev/null +++ b/include/iris/core/ios/utility.h @@ -0,0 +1,77 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +/** + * This is an incomplete file and is intended to be included in + * mac_ios_utility.mm + * + * *DO NOT* include this file directly, use macos_ios_utility.h + */ + +#import +#import + +#include "core/exception.h" +#include "graphics/ios/metal_view.h" + +namespace +{ + +MetalView *get_view() +{ + const auto *window = [[[UIApplication sharedApplication] windows] objectAtIndex:0]; + if (window == nullptr) + { + throw iris::Exception("unable to get main window"); + } + + auto *view = static_cast([[window rootViewController] view]); + if (view == nullptr) + { + throw iris::Exception("unable to get metal view"); + } + + return view; +} + +} + +namespace iris::core::utility +{ + +id metal_device() +{ + const auto *view = get_view(); + + auto *device = [view device]; + if (device == nullptr) + { + throw Exception("unable to get metal device from view"); + } + + return device; +} + +CAMetalLayer *metal_layer() +{ + const auto *view = get_view(); + + const auto *device = [view device]; + if (device == nullptr) + { + throw Exception("unable to get metal device from view"); + } + + auto *layer = [view metalLayer]; + if (layer == nullptr) + { + throw Exception("unable to get layer from view"); + } + + return (CAMetalLayer *)layer; +} + +} diff --git a/include/iris/core/looper.h b/include/iris/core/looper.h new file mode 100644 index 00000000..20868c77 --- /dev/null +++ b/include/iris/core/looper.h @@ -0,0 +1,78 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +namespace iris +{ + +/** + * This class provides a game looper. It takes two functions, one that is called + * at a fixed time step and another that is run as frequently as possible. This + * is based on the https://gafferongames.com/post/fix_your_timestep/ article. + */ +class Looper +{ + public: + /** + * Definition of a function to run in loop. + * + * @param clock + * Total elapsed time since loop started. + * + * @param delta + * Duration of frame. + * + * @returns + * True if loop should continue, false if it should exit. + */ + using LoopFunction = std::function; + + /** + * Construct a new looper. + * + * @param clock + * Start time of looping. + * + * @param timestep + * How frequently to call the fixed time step function. + * + * @param fixed_timestep + * Function to call at the supplied fixed timestep. + * + * @param variable_timestep + * Function to call as frequently as possible. + */ + Looper( + std::chrono::microseconds clock, + std::chrono::microseconds timestep, + LoopFunction fixed_timestep, + LoopFunction variable_timestep); + + /** + * Run the loop. Will continue until one of the supplied functions + * returns false. Clock time will start incrementing from this call. + */ + void run(); + + private: + /** Elapsed time of loop. */ + std::chrono::microseconds clock_; + + /** Fixed time step. */ + std::chrono::microseconds timestep_; + + /** Function to run at foxed time step. */ + LoopFunction fixed_timestep_; + + /** Function to run at variable time step. */ + LoopFunction variable_timestep_; +}; + +} diff --git a/include/iris/core/macos/macos_ios_utility.h b/include/iris/core/macos/macos_ios_utility.h new file mode 100644 index 00000000..f1e34190 --- /dev/null +++ b/include/iris/core/macos/macos_ios_utility.h @@ -0,0 +1,43 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#import + +namespace iris::core::utility +{ + +/** + * Helper function to convert a std::string to a NSString. + * + * @param str + * String to convert. + * + * @returns + * Supplied string converted to NSString. + */ +NSString *string_to_nsstring(const std::string &str); + +/** + * Get the metal device object for the current view. + * + * @returns + * Metal device. + */ +id metal_device(); + +/** + * Get the metal layer for the current view. + * + * @returns + * Metal layer. + */ +CAMetalLayer *metal_layer(); + +} diff --git a/include/iris/core/macos/utility.h b/include/iris/core/macos/utility.h new file mode 100644 index 00000000..7fa75ff9 --- /dev/null +++ b/include/iris/core/macos/utility.h @@ -0,0 +1,45 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +/** + * This is an incomplete file and is intended to be included in + * mac_ios_utility.mm + * + * *DO NOT* include this file directly, use macos_ios_utility.h + */ + +#import + +#include "core/exception.h" + +namespace iris::core::utility +{ + +id metal_device() +{ + return ::CGDirectDisplayCopyCurrentMetalDevice(::CGMainDisplayID()); +} + +CAMetalLayer *metal_layer() +{ + // get a pointer to the main window + auto *window = [[NSApp windows] firstObject]; + if (window == nullptr) + { + throw Exception("could not get main window"); + } + + // get a pointer to the metal layer to render to + auto *layer = static_cast([[window contentView] layer]); + if (layer == nullptr) + { + throw Exception("could not get metal later"); + } + + return layer; +} + +} diff --git a/include/iris/core/matrix4.h b/include/iris/core/matrix4.h new file mode 100644 index 00000000..697f8d47 --- /dev/null +++ b/include/iris/core/matrix4.h @@ -0,0 +1,559 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "core/quaternion.h" +#include "core/utils.h" +#include "core/vector3.h" + +namespace iris +{ + +/** + * Class represents a 4x4 matrix. + * + * This is a header only class to allow for constexpr methods. + */ +class Matrix4 +{ + public: + /** + * Constructs a new identity Matrix4. + */ + constexpr Matrix4() + : elements_({{1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f}}) + { + } + + /** + * Constructs a new matrix with the supplied (row-major) values. + * + * @param elements + * Row major elements. + */ + constexpr explicit Matrix4(const std::array &elements) + : elements_(elements) + { + } + + /** + * Construct a new Matrix4 which represents a rotation by the + * supplied quaternion. + * + * @param rotation + * Rotation to represent. + */ + constexpr explicit Matrix4(const Quaternion &rotation) + : Matrix4() + { + elements_[0] = 1.0f - 2.0f * rotation.y * rotation.y - 2.0f * rotation.z * rotation.z; + elements_[1] = 2.0f * rotation.x * rotation.y - 2.0f * rotation.z * rotation.w; + elements_[2] = 2.0f * rotation.x * rotation.z + 2.0f * rotation.y * rotation.w; + + elements_[4] = 2.0f * rotation.x * rotation.y + 2.0f * rotation.z * rotation.w; + elements_[5] = 1.0f - 2.0f * rotation.x * rotation.x - 2.0f * rotation.z * rotation.z; + elements_[6] = 2.0f * rotation.y * rotation.z - 2.0f * rotation.x * rotation.w; + + elements_[8] = 2.0f * rotation.x * rotation.z - 2.0f * rotation.y * rotation.w; + elements_[9] = 2.0f * rotation.y * rotation.z + 2.0f * rotation.x * rotation.w; + elements_[10] = 1.0f - 2.0f * rotation.x * rotation.x - 2.0f * rotation.y * rotation.y; + + elements_[15] = 1.0f; + } + + /** + * Construct a new Matrix4 which represents a rotation and translation + * by the supplied Quaternion and vector. + * + * @param rotation + * Rotation to represent. + * + * @param translation + * Translation to represent. + */ + constexpr Matrix4(const Quaternion &rotation, const Vector3 &translation) + : Matrix4(rotation) + { + elements_[3u] = translation.x; + elements_[7u] = translation.y; + elements_[11u] = translation.z; + } + + /** + * Static method to create an orthographic projection matrix. + * + * @param width + * width of window. + * + * @param height + * height of window. + * + * @param depth + * Depth of rendering view. + * + * @returns + * An orthographic projection matrix. + */ + constexpr static Matrix4 make_orthographic_projection(float width, float height, float depth) + { + Matrix4 m{}; + + const auto right = width; + const auto left = -right; + const auto top = height; + const auto bottom = -top; + const auto far_plane = depth; + const auto near_plane = -far_plane; + + m.elements_ = { + {2.0f / (right - left), + 0.0f, + 0.0f, + -(right + left) / (right - left), + 0.0f, + 2.0f / (top - bottom), + 0.0f, + -(top + bottom) / (top - bottom), + 0.0f, + 0.0f, + -2.0f / (far_plane - near_plane), + -(far_plane + near_plane) / (far_plane - near_plane), + 0.0f, + 0.0f, + 0.0f, + 1.0f}}; + + return m; + } + + /** + * Static method to create a perspective projection matrix. + * + * @param fov + * Field of view. + * + * @param width + * Width of projection. + * + * @param height + * Height of projection. + * + * @param near_plane + * Near clipping plane. + * + * @param far_plane + * Far clipping plane. + * + * @returns + * A perspective projection matrix. + */ + static Matrix4 make_perspective_projection(float fov, float width, float height, float near_plane, float far_plane) + { + Matrix4 m; + + const auto aspect_ratio = width / height; + const auto tmp = std::tanf(fov / 2.0f); + const auto t = tmp * near_plane; + const auto b = -t; + const auto r = t * aspect_ratio; + const auto l = b * aspect_ratio; + + m.elements_ = { + {(2.0f * near_plane) / (r - l), + 0.0f, + (r + l) / (r - l), + 0.0f, + 0.0f, + (2.0f * near_plane) / (t - b), + (t + b) / (t - b), + 0.0f, + 0.0f, + 0.0f, + -(far_plane + near_plane) / (far_plane - near_plane), + -(2.0f * far_plane * near_plane) / (far_plane - near_plane), + 0.0f, + 0.0f, + -1.0f, + 0.0f}}; + + return m; + } + + /** + * Make a Matrix4 that can be used as a view matrix for a camera. + * + * @param eye + * Position of the camera. + * + * @param look_at + * The point where the camera is looking. + * + * @param up + * The up vector of the camera. + * + * @returns + * A Matrix4 that can be used as a camera view matrix. + */ + static Matrix4 make_look_at(const Vector3 &eye, const Vector3 &look_at, const Vector3 &up) + { + const auto f = Vector3::normalise(look_at - eye); + const auto up_normalised = Vector3::normalise(up); + + const auto s = Vector3::cross(f, up_normalised).normalise(); + const auto u = Vector3::cross(s, f).normalise(); + + Matrix4 m; + + m.elements_ = {{s.x, s.y, s.z, 0.0f, u.x, u.y, u.z, 0.0f, -f.x, -f.y, -f.z, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f}}; + + return m * make_translate(-eye); + } + + /** + * Static method to create a scale matrix. + * + * @param scale + * Vector3 specifying amount to scale along each axis. + * + * @returns + * Scale transformation matrix. + */ + constexpr static Matrix4 make_scale(const Vector3 &scale) + { + Matrix4 m; + + m.elements_ = { + {scale.x, 0.0f, 0.0f, 0.0f, 0.0f, scale.y, 0.0f, 0.0f, 0.0f, 0.0f, scale.z, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f}}; + + return m; + } + + /** + * Static method to create translation matrix. + * + * @param translate + * Vector to translate by. + */ + constexpr static Matrix4 make_translate(const Vector3 &translate) + { + Matrix4 m; + + m.elements_ = { + {1.0f, + 0.0f, + 0.0f, + translate.x, + 0.0f, + 1.0f, + 0.0f, + translate.y, + 0.0f, + 0.0f, + 1.0f, + translate.z, + 0.0f, + 0.0f, + 0.0f, + 1.0f}}; + + return m; + } + + /** + * Invert a matrix. This produces a matrix such that: + * M * invert(M) == Matrix4{ } + * + * @param m + * Matrix to invert. + * + * @returns + * Inverted matrix. + */ + constexpr static Matrix4 invert(const Matrix4 &m) + { + Matrix4 inv{}; + + inv[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] - m[9] * m[6] * m[15] + m[9] * m[7] * m[14] + + m[13] * m[6] * m[11] - m[13] * m[7] * m[10]; + + inv[4] = -m[4] * m[10] * m[15] + m[4] * m[11] * m[14] + m[8] * m[6] * m[15] - m[8] * m[7] * m[14] - + m[12] * m[6] * m[11] + m[12] * m[7] * m[10]; + + inv[8] = m[4] * m[9] * m[15] - m[4] * m[11] * m[13] - m[8] * m[5] * m[15] + m[8] * m[7] * m[13] + + m[12] * m[5] * m[11] - m[12] * m[7] * m[9]; + + inv[12] = -m[4] * m[9] * m[14] + m[4] * m[10] * m[13] + m[8] * m[5] * m[14] - m[8] * m[6] * m[13] - + m[12] * m[5] * m[10] + m[12] * m[6] * m[9]; + + inv[1] = -m[1] * m[10] * m[15] + m[1] * m[11] * m[14] + m[9] * m[2] * m[15] - m[9] * m[3] * m[14] - + m[13] * m[2] * m[11] + m[13] * m[3] * m[10]; + + inv[5] = m[0] * m[10] * m[15] - m[0] * m[11] * m[14] - m[8] * m[2] * m[15] + m[8] * m[3] * m[14] + + m[12] * m[2] * m[11] - m[12] * m[3] * m[10]; + + inv[9] = -m[0] * m[9] * m[15] + m[0] * m[11] * m[13] + m[8] * m[1] * m[15] - m[8] * m[3] * m[13] - + m[12] * m[1] * m[11] + m[12] * m[3] * m[9]; + + inv[13] = m[0] * m[9] * m[14] - m[0] * m[10] * m[13] - m[8] * m[1] * m[14] + m[8] * m[2] * m[13] + + m[12] * m[1] * m[10] - m[12] * m[2] * m[9]; + + inv[2] = m[1] * m[6] * m[15] - m[1] * m[7] * m[14] - m[5] * m[2] * m[15] + m[5] * m[3] * m[14] + + m[13] * m[2] * m[7] - m[13] * m[3] * m[6]; + + inv[6] = -m[0] * m[6] * m[15] + m[0] * m[7] * m[14] + m[4] * m[2] * m[15] - m[4] * m[3] * m[14] - + m[12] * m[2] * m[7] + m[12] * m[3] * m[6]; + + inv[10] = m[0] * m[5] * m[15] - m[0] * m[7] * m[13] - m[4] * m[1] * m[15] + m[4] * m[3] * m[13] + + m[12] * m[1] * m[7] - m[12] * m[3] * m[5]; + + inv[14] = -m[0] * m[5] * m[14] + m[0] * m[6] * m[13] + m[4] * m[1] * m[14] - m[4] * m[2] * m[13] - + m[12] * m[1] * m[6] + m[12] * m[2] * m[5]; + + inv[3] = -m[1] * m[6] * m[11] + m[1] * m[7] * m[10] + m[5] * m[2] * m[11] - m[5] * m[3] * m[10] - + m[9] * m[2] * m[7] + m[9] * m[3] * m[6]; + + inv[7] = m[0] * m[6] * m[11] - m[0] * m[7] * m[10] - m[4] * m[2] * m[11] + m[4] * m[3] * m[10] + + m[8] * m[2] * m[7] - m[8] * m[3] * m[6]; + + inv[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] + m[4] * m[1] * m[11] - m[4] * m[3] * m[9] - + m[8] * m[1] * m[7] + m[8] * m[3] * m[5]; + + inv[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] - m[4] * m[1] * m[10] + m[4] * m[2] * m[9] + + m[8] * m[1] * m[6] - m[8] * m[2] * m[5]; + + auto det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12]; + + if (det != 0.0f) + { + det = 1.0f / det; + + for (auto i = 0; i < 16; i++) + { + inv[i] *= det; + } + } + + return inv; + } + + /** + * Transpose a matrix. + * + * @param matrix + * Matrix to transpose. + * + * @returns + * Transposed matrix. + */ + constexpr static Matrix4 transpose(const Matrix4 &matrix) + { + auto m{matrix}; + + std::swap(m[1], m[4]); + std::swap(m[2], m[8]); + std::swap(m[3], m[12]); + std::swap(m[6], m[9]); + std::swap(m[7], m[13]); + std::swap(m[11], m[14]); + + return m; + } + + /** + * Performs matrix multiplication. + * + * @param matrix + * The Matrix4 to multiply. + * + * @returns + * This Matrix4 multiplied the supplied Matrix4. + */ + constexpr Matrix4 &operator*=(const Matrix4 &matrix) + { + const auto e = elements_; + + const auto calculate_cell = [&e, &matrix](std::size_t row_num, std::size_t col_num) + { + return (e[row_num + 0u] * matrix[col_num + 0u]) + (e[row_num + 1u] * matrix[col_num + 4u]) + + (e[row_num + 2u] * matrix[col_num + 8u]) + (e[row_num + 3u] * matrix[col_num + 12u]); + }; + + elements_[0u] = calculate_cell(0u, 0u); + elements_[1u] = calculate_cell(0u, 1u); + elements_[2u] = calculate_cell(0u, 2u); + elements_[3u] = calculate_cell(0u, 3u); + + elements_[4u] = calculate_cell(4u, 0u); + elements_[5u] = calculate_cell(4u, 1u); + elements_[6u] = calculate_cell(4u, 2u); + elements_[7u] = calculate_cell(4u, 3u); + + elements_[8u] = calculate_cell(8u, 0u); + elements_[9u] = calculate_cell(8u, 1u); + elements_[10u] = calculate_cell(8u, 2u); + elements_[11u] = calculate_cell(8u, 3u); + + elements_[12u] = calculate_cell(12u, 0u); + elements_[13u] = calculate_cell(12u, 1u); + elements_[14u] = calculate_cell(12u, 2u); + elements_[15u] = calculate_cell(12u, 3u); + + return *this; + } + + /** + * Performs Matrix4 multiplication. + * + * @param matrix + * The Matrix4 to multiply. + * + * @returns + * New Matrix4 which is this Matrix4 multiplied the supplied Matrix4. + */ + constexpr Matrix4 operator*(const Matrix4 &matrix) const + { + return Matrix4(*this) *= matrix; + } + + /** + * Multiply this matrix by a given vector3. + * + * Internally this extends the Vector3 to have a fourth element with + * a value of 1.0 + * + * @param vector + * Vector3 to multiply by. + * + * @returns + * This matrix multiplied by the supplied vector3. + */ + constexpr Vector3 operator*(const Vector3 &vector) const + { + return { + vector.x * elements_[0] + vector.y * elements_[1] + vector.z * elements_[2] + elements_[3], + + vector.x * elements_[4] + vector.y * elements_[5] + vector.z * elements_[6] + elements_[7], + + vector.x * elements_[8] + vector.y * elements_[9] + vector.z * elements_[10] + elements_[11], + }; + } + + /** + * Get a reference to the element at the supplied index. + * + * @param index + * Index of element to get. + * + * @returns + * Reference to element at supplied index. + */ + constexpr float &operator[](const size_t index) + { + return elements_[index]; + } + + /** + * Get a copy of the element at the supplied index. + * + * @param index + * Index of element to get. + * + * @returns + * Copy of element at supplied index. + */ + constexpr float operator[](const size_t index) const + { + return elements_[index]; + } + + /** + * Equality operator. + * + * @param other + * Matrix4 to check for equality. + * + * @returns + * True if both Matrix4 objects are the same, false otherwise. + */ + bool operator==(const Matrix4 &other) const + { + return std::equal(std::cbegin(elements_), std::cend(elements_), std::cbegin(other.elements_), compare); + } + + /** + * Inequality operator. + * + * @param other + * Matrix4 to check for inequality. + * + * @returns + * True if both Matrix4 objects are not the same, false otherwise. + */ + bool operator!=(const Matrix4 &other) const + { + return !(*this == other); + } + + /** + * Get a pointer to the start of the internal Matrix4 data array. + * + * @returns + * Pointer to start if Matrix4 data. + */ + constexpr const float *data() const + { + return elements_.data(); + } + + /** + * Get a column from the matrix and return as a vector3. This ignores + * the bottom row of the matrix. + * + * @param index + * The index of the column to return. + * + * @returns + * The first three value of the supplied column. + */ + constexpr Vector3 column(const std::size_t index) const + { + return {elements_[index], elements_[index + 4u], elements_[index + 8u]}; + } + + private: + /** Matrix4 data */ + std::array elements_; +}; + +/** + * Writes the Matrix4 to the stream, useful for debugging. + * + * @param out + * The stream to write to. + * + * @param m + * The Matrix4 to write to the stream. + * + * @return + * A reference to the supplied stream, after the Matrix4 has been + * written. + */ +inline std::ostream &operator<<(std::ostream &out, const Matrix4 &m) +{ + out << m[0] << " " << m[1] << " " << m[2] << " " << m[3] << std::endl; + out << m[4] << " " << m[5] << " " << m[6] << " " << m[7] << std::endl; + out << m[8] << " " << m[9] << " " << m[10] << " " << m[11] << std::endl; + out << m[12] << " " << m[13] << " " << m[14] << " " << m[15]; + + return out; +} + +} diff --git a/include/iris/core/quaternion.h b/include/iris/core/quaternion.h new file mode 100644 index 00000000..550b1bff --- /dev/null +++ b/include/iris/core/quaternion.h @@ -0,0 +1,444 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "core/utils.h" +#include "core/vector3.h" + +namespace iris +{ + +/** + * Class representing a Quaternion. + * + * A Quaternion represents a rotation (w) about a vector (x, y, z). + * + * This is a header only class to allow for constexpr methods. + */ +class Quaternion +{ + public: + /** + * Construct a new unit Quaternion. + */ + constexpr Quaternion() + : w(1.0f) + , x(0.0f) + , y(0.0f) + , z(0.0f) + { + } + + /** + * Construct a Quaternion which represents a rotation about an axis. + * + * @param axis + * The axis about which to rotate. + * + * @param angle + * The rotation in radians. + */ + Quaternion(const Vector3 &axis, float angle) + : w(0.0f) + , x(0.0f) + , y(0.0f) + , z(0.0f) + { + const auto half_angle = angle / 2.0f; + const auto sin_angle = std::sin(half_angle); + + w = std::cos(half_angle); + x = sin_angle * axis.x; + y = sin_angle * axis.y; + z = sin_angle * axis.z; + + normalise(); + } + + /** + * Construct a Quaternion with supplied components. + * + * @param x + * x component. + * + * @param y + * x component. + * + * @param z + * x component. + * + * @param w + * x component. + */ + constexpr Quaternion(float x, float y, float z, float w) + : w(w) + , x(x) + , y(y) + , z(z) + { + } + + /** + * Construct a Quaternion with Euler angles. + * + * @param yaw + * Yaw angle in radians. + * + * @param pitch + * Pitch angle in radians. + * + * @param roll + * Roll angle in radians. + */ + Quaternion(float yaw, float pitch, float roll) + : Quaternion() + { + const auto cy = std::cos(yaw * 0.5f); + const auto sy = std::sin(yaw * 0.5f); + const auto cp = std::cos(pitch * 0.5f); + const auto sp = std::sin(pitch * 0.5f); + const auto cr = std::cos(roll * 0.5f); + const auto sr = std::sin(roll * 0.5f); + + w = cr * cp * cy + sr * sp * sy; + x = sr * cp * cy - cr * sp * sy; + y = cr * sp * cy + sr * cp * sy; + z = cr * cp * sy - sr * sp * cy; + } + + /** + * Multiply this Quaternion by another, therefore applying a composition + * of both rotations. + * + * @param quaternion + * Quaternion to compose with this. + * + * @returns + * Reference to this Quaternion. + */ + constexpr Quaternion &operator*=(const Quaternion &quaternion) + { + const Quaternion copy{*this}; + + w = copy.w * quaternion.w - copy.x * quaternion.x - copy.y * quaternion.y - copy.z * quaternion.z; + x = copy.w * quaternion.x + copy.x * quaternion.w + copy.y * quaternion.z - copy.z * quaternion.y; + y = copy.w * quaternion.y + copy.y * quaternion.w + copy.z * quaternion.x - copy.x * quaternion.z; + z = copy.w * quaternion.z + copy.z * quaternion.w + copy.x * quaternion.y - copy.y * quaternion.x; + + return *this; + } + + /** + * Create a new Quaternion which is the composition of this this + * rotation with another. + * + * @param quaternion + * Quaternion to compose with this. + * + * @returns + * Copy of this Quaternion composed with the supplied one. + */ + constexpr Quaternion operator*(const Quaternion &quaternion) const + { + return Quaternion{*this} *= quaternion; + } + + /** + * Add a rotation specified as a Vector3 to this Quaternion. + * + * @param vector + * Vector to add. + * + * @returns + * Reference to this Quaternion. + */ + constexpr Quaternion &operator+=(const Vector3 &vector) + { + Quaternion q{}; + q.w = 0.0f; + q.x = vector.x; + q.y = vector.y; + q.z = vector.z; + + q *= *this; + + w += q.w / 2.0f; + x += q.x / 2.0f; + y += q.y / 2.0f; + z += q.z / 2.0f; + + return *this; + } + + /** + * Create a new Quaternion that is the composition of this rotation + * and one specified as a vector3. + * + * @param vector + * Vector to add. + * + * @returns + * Copy of this Quaternion composed with the Vector3 rotation. + */ + constexpr Quaternion operator+(const Vector3 &vector) const + { + return Quaternion{*this} += vector; + } + + /** + * Create a new Quaternion that is this value scaled. + * + * @param scale + * Amount to scale by. + * + * @returns + * Scaled quaternion. + */ + constexpr Quaternion operator*(float scale) const + { + return Quaternion{*this} *= scale; + } + + /** + * Scale quaternion. + * + * @param scale + * Amount to scale by. + * + * @returns + * Reference to this Quaternion. + */ + constexpr Quaternion &operator*=(float scale) + { + x *= scale; + y *= scale; + z *= scale; + w *= scale; + + return *this; + } + + /** + * Create a new Quaternion that is this Quaternion subtracted with a + * supplied Quaternion. + * + * @param other + * Quaternion to subtract from this. + * + * @returns + * Copy of this Quaternion after subtraction. + */ + constexpr Quaternion operator-(const Quaternion &other) const + { + return Quaternion{*this} -= other; + } + + /** + * Subtract a Quaternion from this. + * + * @param other + * Quaternion to subtract from this. + * + * @returns + * Reference to this Quaternion. + */ + constexpr Quaternion &operator-=(const Quaternion &other) + { + return *this += -other; + } + + /** + * Create a new Quaternion that is this Quaternion added with a + * supplied Quaternion. + * + * @param other + * Quaternion to add to this. + * + * @returns + * Copy of this Quaternion after addition. + */ + constexpr Quaternion operator+(const Quaternion &other) const + { + return Quaternion{*this} += other; + } + + /** + * Add a Quaternion from this. + * + * @param other + * Quaternion to subtract from this. + * + * @returns + * Reference to this Quaternion. + */ + constexpr Quaternion &operator+=(const Quaternion &other) + { + x += other.x; + y += other.y; + z += other.z; + w += other.w; + + return *this; + } + + /** + * Negate operator. + * + * @returns + * Copy of this Quaternion with each component negated. + */ + constexpr Quaternion operator-() const + { + return {-x, -y, -z, -w}; + } + + /** + * Calculate the Quaternion dot product. + * + * @param other + * Quaternion to calculate dot product with. + * + * @returns + * Dot product of this Quaternion with the supplied one. + */ + constexpr float dot(const Quaternion &other) const + { + return x * other.x + y * other.y + z * other.z + w * other.w; + } + + /** + * Perform spherical linear interpolation toward target Quaternion. + * + * @param target + * Quaternion to interpolate towards. + * + * @param amount + * Amount to interpolate, must be in range [0.0, 1.0]. + */ + constexpr void slerp(Quaternion target, float amount) + { + auto dot = this->dot(target); + if (dot < 0.0f) + { + target = -target; + dot = -dot; + } + + const auto threshold = 0.9995f; + if (dot > threshold) + { + *this = *this + ((target - *this) * amount); + normalise(); + } + else + { + const auto theta_0 = std::acos(dot); + const auto theta = theta_0 * amount; + const auto sin_theta = std::sin(theta); + const auto sin_theta_0 = std::sin(theta_0); + + const auto s0 = std::cos(theta) - dot * sin_theta / sin_theta_0; + const auto s1 = sin_theta / sin_theta_0; + + *this = (*this * s0) + (target * s1); + } + } + + /** + * Equality operator. + * + * @param other + * Quaternion to check for equality. + * + * @returns + * True if both Quaternion objects are the same, false otherwise. + */ + bool operator==(const Quaternion &other) const + { + return compare(w, other.w) && compare(x, other.x) && compare(y, other.y) && compare(z, other.z); + } + + /** + * Inequality operator. + * + * @param other + * Quaternion to check for inequality. + * + * @returns + * True if both Quaternion objects are not the same, false otherwise. + */ + bool operator!=(const Quaternion &other) const + { + return !(*this == other); + } + + /** + * Normalise this Quaternion. + * + * @returns + * A reference to this Quaternion. + */ + Quaternion &normalise() + { + const auto magnitude = std::pow(w, 2.0f) + std::pow(x, 2.0f) + std::pow(y, 2.0f) + std::pow(z, 2.0f); + + if (magnitude == 0.0f) + { + w = 1.0f; + } + else + { + const auto d = std::sqrt(magnitude); + + w /= d; + x /= d; + y /= d; + z /= d; + } + + return *this; + } + + /** Angle of rotation. */ + float w; + + /** x axis of rotation. */ + float x; + + /** y axis of rotation. */ + float y; + + /** z axis of rotation. */ + float z; +}; + +/** + * Write a Quaternion to a stream, useful for debugging. + * + * @param out + * Stream to write to. + * + * @param q + * Quaternion to write to stream. + * + * @returns + * Reference to input stream. + */ +inline std::ostream &operator<<(std::ostream &out, const Quaternion &q) +{ + out << "x: " << q.x << " " + << "y: " << q.y << " " + << "z: " << q.z << " " + << "w: " << q.w; + + return out; +} + +} diff --git a/include/iris/core/random.h b/include/iris/core/random.h new file mode 100644 index 00000000..24612144 --- /dev/null +++ b/include/iris/core/random.h @@ -0,0 +1,99 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** + * Generate a uniform random integer in the range [min, max]. + * + * @param min + * Minimum value. + * + * @param max + * Maximum value. + * + * @returns + * Random integer. + */ +std::uint32_t random_uint32(std::uint32_t min, std::uint32_t max); + +/** + * Generate a uniform random integer in the range [min, max]. + * + * @param min + * Minimum value. + * + * @param max + * Maximum value. + * + * @returns + * Random integer. + */ +std::int32_t random_int32(std::int32_t min, std::int32_t max); + +/** + * Generate a uniform random float in the range [min, max). + * + * @param min + * Minimum value. + * + * @param max + * Maximum value. + * + * @returns + * Random integer. + */ +float random_float(float min, float max); + +/** + * Flip a (biased) coin. + * + * @param bias + * Possibility of heads [0.0, 1.0]. A value of 0.5 is a fair coin toss. + * + * @returns + * True if heads, false if tails. + */ +bool flip_coin(float bias = 0.5f); + +/** + * Get a random element from a collection. + * + * @param collection + * Collection to get element from. + * + * @returns + * Random element. + */ +template +typename T::reference random_element(T &container) +{ + const auto index = random_uint32(0u, container.size() - 1u); + return container[index]; +} + +/** + * Get a random element from a collection. + * + * @param collection + * Collection to get element from. + * + * @returns + * Random element. + */ +template +typename T::const_reference random_element(const T &container) +{ + const auto index = random_uint32(0u, container.size() - 1u); + return container[index]; +} + +} diff --git a/include/iris/core/resource_loader.h b/include/iris/core/resource_loader.h new file mode 100644 index 00000000..23c77a85 --- /dev/null +++ b/include/iris/core/resource_loader.h @@ -0,0 +1,69 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +#include "core/data_buffer.h" + +namespace iris +{ + +/** + * Singleton class for loading and caching resources. + */ +class ResourceLoader +{ + public: + /** + * Get single instance of class. + * + * @returns + * Reference to single instance. + */ + static ResourceLoader &instance(); + + /** + * Load a resource. If this is the first load of resource then it is + * fetched from the platform specific backing store (e.g. disk). + * Otherwise a cached copy is returned. + * + * @param resource + * Name of resource. This should be a '/' separated path relative + * to the root resource location. + * + * @returns + * Const reference to loaded data. + */ + const DataBuffer &load(const std::string &resource); + + /** + * Set root resource location. + * + * @param root + * New root location. + */ + void set_root_directory(const std::filesystem::path &root); + + private: + /** + * Construct a new ResourceLoader. Private to force instantiation + * through instance. + */ + ResourceLoader(); + + /** Cache of loaded resources. */ + std::map resources_; + + /** Resource root. */ + std::filesystem::path root_; +}; + +} diff --git a/include/iris/core/root.h b/include/iris/core/root.h new file mode 100644 index 00000000..4f0e0452 --- /dev/null +++ b/include/iris/core/root.h @@ -0,0 +1,299 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +namespace iris +{ + +class JobSystemManager; +class MeshManager; +class PhysicsManager; +class TextureManager; +class WindowManager; + +/** + * This class allows for the runtime registration and retrieval of various + * manager classes. It is a singleton and therefore provides singleton access to + * the various components in owns without requiring them to be singletons. + * + * These managers are factory classes that can create engine components, the + * reason for all this machinery is: + * - it decouples actual implementation from the Root + * - start() can register all supported mangers for the current platform and + * set sane defaults + * - allows a user to register their own implementations (e.g. for a different + * physics library) + * + * Note that there is a subtle difference between setting the graphics/physics + * apis and the jobs api. Graphics/Physics are entirely a user choice, they may + * want one, both or neither. The Root makes this possible as they only need to + * get the manager for the components they need and call the varies create + * methods. + * + * Whereas physics/graphics are opt-in the jobs api is compulsory. The engine + * has to have a jobs system. Therefore setting the jobs api (set_jobs_api()) + * will actually create the job system. To make things a bit less verbose the + * JobsApiManager interface exposes the two job system api methods, so a user + * can use jobs directly from the jobs_manager() call. + */ +class Root +{ + public: + ~Root() = default; + + /** + * Get the current WindowManager. + * + * @returns + * Current WindowManager. + */ + static WindowManager &window_manager(); + + /** + * Get the current MeshManager. + * + * @returns + * Current MeshManager. + */ + static MeshManager &mesh_manager(); + + /** + * Get the current TextureManager. + * + * @returns + * Current TextureManager. + */ + static TextureManager &texture_manager(); + + /** + * Get the current PhysicsManager. + * + * @returns + * Current PhysicsManager. + */ + static PhysicsManager &physics_manager(); + + /** + * Get the current JobSystemManager. + * + * @returns + * Current JobSystemManager. + */ + static JobSystemManager &jobs_manager(); + + /** + * Register managers for a given api name. + * + * @param api + * Name of api to register managers to, + * + * @param window_manager + * New WindowManager. + * + * @param mesh_manager + * New MeshManager. + * + * @param texture_manager + * New TextureManager. + */ + static void register_graphics_api( + const std::string &api, + std::unique_ptr window_manager, + std::unique_ptr mesh_manager, + std::unique_ptr texture_manager); + + /** + * Get the currently set graphics api. + * + * @returns + * Name of currently set graphics api. + */ + static std::string graphics_api(); + + /** + * Set the current graphics api. + * + * @param api + * New graphics api name. + */ + static void set_graphics_api(const std::string &api); + + /** + * Get a collection of all registered api names. + * + * @returns + * Collection of registered api names. + */ + static std::vector registered_graphics_apis(); + + /** + * Register managers for a given api name. + * + * @param api + * Name of api to register managers to, + * + * @param physics_manager + * New PhysicsManager. + */ + static void register_physics_api(const std::string &api, std::unique_ptr physics_manager); + + /** + * Get the currently set physics api. + * + * @returns + * Name of currently set physics api. + */ + static std::string physics_api(); + + /** + * Set the current physics api. + * + * @param api + * New physics api name. + */ + static void set_physics_api(const std::string &api); + + /** + * Get a collection of all registered api names. + * + * @returns + * Collection of registered api names. + */ + static std::vector registered_physics_apis(); + + /** + * Register managers for a given api name. + * + * @param api + * Name of api to register managers to, + * + * @param jobs_manager + * New JobSystemManager. + */ + static void register_jobs_api(const std::string &api, std::unique_ptr jobs_manager); + + /** + * Get the currently set jobs api. + * + * @returns + * Name of currently set jobs api. + */ + static std::string jobs_api(); + + /** + * Set the current jobs api. + * + * @param api + * New jobs api name. + */ + static void set_jobs_api(const std::string &api); + + /** + * Get a collection of all registered api names. + * + * @returns + * Collection of registered api names. + */ + static std::vector registered_jobs_apis(); + + /** + * Clear all registered components. + * + * This method exists to allow the engine to destroy the internal managers + * at a time of its choosing, rather than waiting for the singleton itself + * to be destroyed. There is no reason a user should have to call this. + */ + static void reset(); + + private: + // private to force access through above public static methods + Root(); + static Root &instance(); + + // these are the member function equivalents of the above static methods + // see their docs for details + + WindowManager &window_manager_impl() const; + + MeshManager &mesh_manager_impl() const; + + TextureManager &texture_manager_impl() const; + + PhysicsManager &physics_manager_impl() const; + + JobSystemManager &jobs_manager_impl() const; + + void register_graphics_api_impl( + const std::string &api, + std::unique_ptr window_manager, + std::unique_ptr mesh_manager, + std::unique_ptr texture_manager); + + std::string graphics_api_impl() const; + + void set_graphics_api_impl(const std::string &api); + + std::vector registered_graphics_apis_impl() const; + + void register_physics_api_impl(const std::string &api, std::unique_ptr physics_manager); + + std::string physics_api_impl() const; + + void set_physics_api_impl(const std::string &api); + + std::vector registered_physics_apis_impl() const; + + void register_jobs_api_impl(const std::string &api, std::unique_ptr jobs_manager); + + std::string jobs_api_impl() const; + + void set_jobs_api_impl(const std::string &api); + + std::vector registered_jobs_apis_impl() const; + + void reset_impl(); + + /** + * Helper struct encapsulating all managers for a graphics api. + * + * Note that the member order is important, we want the WindowManager to + * be destroyed first as some implementations require the Renderer + * destructor to wait for gpu operations to finish before destroying other + * resources. + */ + struct GraphicsApiManagers + { + std::unique_ptr mesh_manager; + std::unique_ptr texture_manager; + std::unique_ptr window_manager; + }; + + /** Map of graphics api name to managers. */ + std::unordered_map graphics_api_managers_; + + /** Name of current graphics api. */ + std::string graphics_api_; + + /** Map of physics api name to managers. */ + std::unordered_map> physics_api_managers_; + + /** Name of current physics api. */ + std::string physics_api_; + + /** Map of jobs api name to managers. */ + std::unordered_map> jobs_api_managers_; + + /** Name of current jobs api. */ + std::string jobs_api_; +}; + +} diff --git a/include/iris/core/semaphore.h b/include/iris/core/semaphore.h new file mode 100644 index 00000000..b1f75625 --- /dev/null +++ b/include/iris/core/semaphore.h @@ -0,0 +1,55 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +namespace iris +{ + +/** + * A syntonisation primitive which contains a counter that can be incremented + * (release) or decremented (acquire) by any thread. Attempting to acquire a + * semaphore when the internal count is 0 will block the calling thread until + * another calls release. + */ +class Semaphore +{ + public: + /** + * Create a new Semaphore with an initial value for the counter. + * + * @param initial + * Initial value for counter. + */ + Semaphore(std::ptrdiff_t initial = 0); + + ~Semaphore(); + + Semaphore(const Semaphore &) = delete; + Semaphore &operator=(const Semaphore &) = delete; + Semaphore(Semaphore &&); + Semaphore &operator=(Semaphore &&); + + /** + * Increment counter and unblock any waiting threads. + */ + void release(); + + /** + * Decrement counter or block until it can. + */ + void acquire(); + + private: + /** Pointer to implementation. */ + struct implementation; + std::unique_ptr impl_; +}; + +} diff --git a/include/iris/core/start.h b/include/iris/core/start.h new file mode 100644 index 00000000..0e7fd6f3 --- /dev/null +++ b/include/iris/core/start.h @@ -0,0 +1,45 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** + * Start the engine! This performs all platform and subsystem initialisation + * before calling the supplied entry. + * + * @param argc + * argc from main() + * + * @param argv + * argv from main() + * + * @param entry + * Entry point into game, will be passed argc and argv back. + */ +void start(int argc, char **argv, std::function entry); + +/** + * Start the engine! This performs all platform and subsystem initialisation + * before calling the supplied entry. This enables additional debugging and is + * mainly used for diagnosing engine issues. + * + * @param argc + * argc from main() + * + * @param argv + * argv from main() + * + * @param entry + * Entry point into game, will be passed argc and argv back. + */ +void start_debug(int argc, char **argv, std::function entry); + +} diff --git a/include/iris/core/static_buffer.h b/include/iris/core/static_buffer.h new file mode 100644 index 00000000..1405a6df --- /dev/null +++ b/include/iris/core/static_buffer.h @@ -0,0 +1,68 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +namespace iris +{ + +/** + * Fixed size managed buffer. Memory is allocated in pages and is read-writable. + * Guard pages are also allocated before and after the memory region, so + * out-of-bounds access fails fast and loud. + */ +class StaticBuffer +{ + public: + /** + * Create buffer with requested number of pages. + * + * @param pages + * Number of pages to allocate. + */ + explicit StaticBuffer(std::size_t pages); + + /** + * Release all allocated memory. + */ + ~StaticBuffer(); + + /** + * Get page size in bytes. + * + * @returns + * Number of bytes in a page. + */ + static std::size_t page_size(); + + /** + * Get start of allocated buffer. + * + * @returns + * Pointer to start of buffer. + */ + operator std::byte *() const; + + /** + * Number of allocated bytes. This does not include any guard pages. + * + * @returns + * Number of allocated bytes. + */ + std::size_t size() const; + + private: + /** Pointer to implementation. */ + struct implementation; + std::unique_ptr impl_; +}; + +} diff --git a/include/iris/core/thread.h b/include/iris/core/thread.h new file mode 100644 index 00000000..6c6ede4a --- /dev/null +++ b/include/iris/core/thread.h @@ -0,0 +1,81 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +namespace iris +{ + +/** + * Class encapsulating a thread. In general this is an internal class. For + * parallelising work the JobSystem should be used. + */ +class Thread +{ + public: + /** + * Construct a new thread not associated with any function. + */ + Thread(); + + /** + * Construct a thread and run the supplied function and args. + * + * @param function + * Function to run in thread. + * + * @param args + * Arguments of function, will be perfectly forwarded. + */ + template + Thread(Function &&function, Args &&...args) + : thread_(std::forward(function), std::forward(args)...) + { + } + + /** + * Checks if this thread is currently active. + * + * @returns + * True if this thread has started and not yet been joined, else + * False. + */ + bool joinable() const; + + /** + * Block and wait for this thread to finish executing. + */ + void join(); + + /** + * Get id of thread. + * + * @returns + * Thread id. + */ + std::thread::id get_id() const; + + /** + * Bind this thread such that it only executes on the specified core, + * preventing the kernel from scheduling it onto another core. + * + * Note that depending on the current platform this may act as a + * suggestion to the kernel, rather than be honored. + * + * @param core + * Id of core to bind to in the range [0, number of cores) + */ + void bind_to_core(std::size_t core); + + private: + /** Internal thread object. */ + std::thread thread_; +}; + +} diff --git a/include/iris/core/transform.h b/include/iris/core/transform.h new file mode 100644 index 00000000..fd0ef1f8 --- /dev/null +++ b/include/iris/core/transform.h @@ -0,0 +1,209 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "core/matrix4.h" +#include "core/quaternion.h" +#include "core/vector3.h" + +namespace iris +{ + +/** + * Class representing transformation in 3D space. A transformation is formed of + * a translation, rotation and scale. Conceptually this class provides a more + * semantically specific api over a Matrix4. + */ +class Transform +{ + public: + /** + * Construct an empty Transform, this represents zero translation, zero + * rotation and scale of 1. + */ + Transform(); + + /** + * Construct a Transform from an existing Matrix4. + * + * @param matrix + * Matrix representing transform. + */ + explicit Transform(const Matrix4 &matrix); + + /** + * Construct a transform from component parts. + * + * @param translation + * Transform translation. + * + * @param rotation + * Transform rotation. + * + * @param scale + * Transform scale. + */ + Transform(const Vector3 &translation, const Quaternion &rotation, const Vector3 &scale); + + /** + * Get the matrix which represents this transformation. + * + * @returns + * Transformation matrix. + */ + Matrix4 matrix() const; + + /** + * Set matrix. Translation, rotation and scale will be derived from input. + * + * @param matrix + * New matrix. + */ + void set_matrix(const Matrix4 &matrix); + + /** + * Interpolate between this and another Transform. + * + * @param other + * Transform to interpolate to. + * + * @param amount + * Interpolation amount, must be in range [0.0, 1.0]. + */ + void interpolate(const Transform &other, float amount); + + /** + * Get the translation component of the transform. + * + * @returns + * Translation component. + */ + Vector3 translation() const; + + /** + * Set translation. + * + * @param translation + * New translation. + */ + void set_translation(const Vector3 &translation); + + /** + * Get the rotation component of the transform. + * + * @returns + * Rotation component. + */ + Quaternion rotation() const; + + /** + * Set rotation. + * + * @param rotation + * New rotation. + */ + void set_rotation(const Quaternion &rotation); + + /** + * Get the scale component of the transform. + * + * @returns + * Scale component. + */ + Vector3 scale() const; + + /** + * Set scale. + * + * @param scale + * New scale. + */ + void set_scale(const Vector3 &scale); + + /** + * Equality operator. + * + * @param other + * Transform to compare for equality. + * + * @returns + * True if both transform are the same, false otherwise. + */ + bool operator==(const Transform &other) const; + + /** + * Inequality operator. + * + * @param other + * Transform to compare for inequality. + * + * @returns + * True if both transform are not the same, false otherwise. + */ + bool operator!=(const Transform &other) const; + + /** + * Performs transform multiplication. This results in a new transform, which + * is this transform followed by the supplied one. + * + * @param other + * The transform to multiple. + * + * @returns + * New Transform which is this Transform multiplied by the supplied one. + */ + Transform operator*(const Transform &other) const; + + /** + * Performs transform multiplication. This results in a new transform, which + * is this transform followed by the supplied one. + * + * @param other + * The transform to multiple. + * + * @returns + * This Transform after the supplied transform has been applied. + */ + Transform &operator*=(const Transform &other); + + /** + * Performs transform multiplication. This results in a new transform, which + * is this transform followed by the supplied matrix. + * + * @param other + * The matrix to multiple. + * + * @returns + * This new Transform which is this transform multiplied by the supplied + * matrix. + */ + Transform operator*(const Matrix4 &other) const; + + /** + * Performs transform multiplication. This results in a new transform, which + * is this transform followed by the supplied matrix. + * + * @param other + * The matrix to multiple. + * + * @returns + * This Transform after the supplied transform has been applied. + */ + Transform &operator*=(const Matrix4 &other); + + private: + /** Translation component. */ + Vector3 translation_; + + /** Rotation component. */ + Quaternion rotation_; + + /** Scale component. */ + Vector3 scale_; +}; + +} diff --git a/include/iris/core/utils.h b/include/iris/core/utils.h new file mode 100644 index 00000000..4c56a0f6 --- /dev/null +++ b/include/iris/core/utils.h @@ -0,0 +1,28 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** + * Compare two floating point numbers using a scaling epsilon. + * + * @param a + * First float. + * + * @param b + * Second float. + * + * @returns + * True if both floats are equal (within an epsilon), false otherwise. + */ +bool compare(float a, float b); + +} diff --git a/include/iris/core/vector3.h b/include/iris/core/vector3.h new file mode 100644 index 00000000..81898611 --- /dev/null +++ b/include/iris/core/vector3.h @@ -0,0 +1,406 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +#include "core/utils.h" + +namespace iris +{ + +/** + * Class representing a vector in 3D space. Comprises an x, y and z + * component. + * + * This is a header only class to allow for constexpr methods. + */ +class Vector3 +{ + public: + /** + * Constructs a new Vector3 with all components initialised to 0. + */ + constexpr Vector3() + : Vector3(0.0f) + { + } + + /** + * Constructs a new Vector3 with all components initialised to the same + * value. + * + * @param xyz + * Value for x, y and z. + */ + constexpr Vector3(float xyz) + : Vector3(xyz, xyz, xyz) + { + } + + /** + * Constructs a new Vector3 with the supplied components. + * + * @param x + * x component. + * + * @param y + * y component. + * + * @param z + * z component. + */ + constexpr Vector3(float x, float y, float z) + : x(x) + , y(y) + , z(z) + { + } + + /**d221G + * Multiply each component by a scalar value. + * + * @param scale + * scalar value. + * + * @return + * Reference to this vector3. + */ + constexpr Vector3 &operator*=(float scale) + { + x *= scale; + y *= scale; + z *= scale; + + return *this; + } + /** + * Create a new Vector3 which is this Vector3 with each component + * multiplied by a scalar value. + * + * @param scale + * scalar value. + * + * @return + * Copy of this Vector3 with each component multiplied by a + * scalar value. + */ + constexpr Vector3 operator*(float scale) const + { + return Vector3(*this) *= scale; + } + /** + * Component wise add a Vector3 to this vector3. + * + * @param vector + * The Vector3 to add to this. + * + * @return + * Reference to this vector3. + */ + constexpr Vector3 &operator+=(const Vector3 &vector) + { + x += vector.x; + y += vector.y; + z += vector.z; + + return *this; + } + /** + * Create a new Vector3 which is this Vector3 added with a supplied + * vector3. + * + * @param vector + * Vector3 to add to this. + * + * @return + * Copy of this Vector3 with each component added to the + * components of the supplied vector3. + */ + constexpr Vector3 operator+(const Vector3 &vector) const + { + return Vector3(*this) += vector; + } + /** + * Component wise subtract a Vector3 to this vector3. + * + * @param v + * The Vector3 to subtract from this. + * + * @return + * Reference to this vector3. + */ + constexpr Vector3 &operator-=(const Vector3 &vector) + { + *this += -vector; + return *this; + } + /** + * Create a new Vector3 which is this Vector3 subtracted with a + * supplied vector3. + * + * @param v + * Vector3 to subtract from this. + * + * @return + * Copy of this Vector3 with each component subtracted to the + * components of the supplied vector3. + */ + constexpr Vector3 operator-(const Vector3 &vector) const + { + return Vector3(*this) -= vector; + } + /** + * Component wise multiple a Vector3 to this vector3. + * + * @param vector + * The Vector3 to multiply. + * + * @returns + * Reference to this vector3. + */ + constexpr Vector3 &operator*=(const Vector3 &vector) + { + x *= vector.x; + y *= vector.y; + z *= vector.z; + + return *this; + } + /** + * Create a new Vector3 which us this Vector3 component wise multiplied + * with a supplied vector3. + * + * @param vector + * Vector3 to multiply with this. + * + * @returns + * Copy of this Vector3 component wise multiplied with the supplied + * vector3. + */ + constexpr Vector3 operator*(const Vector3 &vector) const + { + return Vector3{*this} *= vector; + } + /** + * Negate operator. + * + * @return + * Return a copy of this Vector3 with each component negated. + */ + constexpr Vector3 operator-() const + { + return Vector3{-x, -y, -z}; + } + /** + * Equality operator. + * + * @param other + * Vector3 to check for equality. + * + * @returns + * True if both Vector3 objects are the same, false otherwise. + */ + bool operator==(const Vector3 &other) const + { + return compare(x, other.x) && compare(y, other.y) && compare(z, other.z); + } + /** + * Inequality operator. + * + * @param other + * Vector3 to check for inequality. + * + * @returns + * True if both Vector3 objects are not the same, false otherwise. + */ + bool operator!=(const Vector3 &other) const + { + return !(other == *this); + } + + /** + * Calculate the vector dot product. + * + * @param vector + * Vector3 to calculate dot product with. + * + * @returns + * Dot product of this vector with supplied one. + */ + constexpr float dot(const Vector3 &vector) const + { + return x * vector.x + y * vector.y + z * vector.z; + } + /** + * Perform cross product of this Vector3 with a supplied one. + * + * @param vector + * Vector3 to cross with. + * + * @return + * Reference to this vector3. + */ + constexpr Vector3 &cross(const Vector3 &vector) + { + const auto i = (y * vector.z) - (z * vector.y); + const auto j = (x * vector.z) - (z * vector.x); + const auto k = (x * vector.y) - (y * vector.x); + + x = i; + y = -j; + z = k; + + return *this; + } + /** + * Normalises this vector3. + * + * @return + * Reference to this vector3. + */ + Vector3 &normalise() + { + const auto length = + + std::sqrt(std::pow(x, 2.0f) + std::pow(y, 2.0f) + std::pow(z, 2.0f)); + + if (length != 0.0f) + { + x /= length; + y /= length; + z /= length; + } + + return *this; + } + /** + * Get the magnitude of this vector. + * + * @return + * Vector magnitude. + */ + float magnitude() const + { + return std::hypot(x, y, z); + } + /** + * Linear interpolate between this and another vector. + * + * @param other + * Vector3 to interpolate to. + * + * @param amount + * Interpolation amount, must be in range [0.0, 1.0]. + */ + constexpr void lerp(const Vector3 &other, float amount) + { + *this *= (1.0f - amount); + *this += (other * amount); + } + /** + * Linear interpolate between two vectors. + * + * @param start + * Vector3 to start from. + * + * @param end + * Vector3 to lerp towards. + * + * @param amount + * Interpolation amount, must be in range [0.0, 1.0]. + * + * @returns + * Result of lerp. + */ + constexpr static Vector3 lerp(const Vector3 &start, const Vector3 &end, float amount) + { + auto tmp = start; + tmp.lerp(end, amount); + + return tmp; + } + /** + * Cross two Vector3 objects with each other. + * + * @param v1 + * First Vector3 to cross. + * + * @param v2 + * Second Vector3 to cross. + * + * @return + * v1 cross v2. + */ + constexpr static Vector3 cross(const Vector3 &v1, const Vector3 &v2) + { + return Vector3(v1).cross(v2); + } + /** + * Normalise a supplied vector3. + * + * @param vector + * Vector3 to normalise. + * + * @return + * Supplied Vector3 normalised. + */ + static Vector3 normalise(const Vector3 &vector) + { + return Vector3(vector).normalise(); + } + /** + * Get the Euclidian distance between two vectors. + * + * @param a + * Point to calculate distance from. + * + * @param b + * Point to calculate distance to. + * + * @returns + * Distance between point a and b. + */ + static float distance(const Vector3 &a, const Vector3 &b) + { + return (b - a).magnitude(); + } + /** x component */ + float x; + + /** y component */ + float y; + + /** z component */ + float z; +}; + +/** + * Write a Vector3 to a stream, useful for debugging. + * + * @param out + * Stream to write to. + * + * @param v + * Vector3 to write to stream. + * + * @return + * Reference to input stream. + */ +inline std::ostream &operator<<(std::ostream &out, const Vector3 &v) +{ + out << "x: " << v.x << " " + << "y: " << v.y << " " + << "z: " << v.z; + + return out; +} + +} diff --git a/include/iris/events/event.h b/include/iris/events/event.h new file mode 100644 index 00000000..87727ac5 --- /dev/null +++ b/include/iris/events/event.h @@ -0,0 +1,209 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "core/exception.h" +#include "events/event_type.h" +#include "events/keyboard_event.h" +#include "events/mouse_button_event.h" +#include "events/mouse_event.h" +#include "events/quit_event.h" +#include "events/touch_event.h" + +namespace iris +{ + +/** + * Class encapsulating a user input event. This provides common access to + * specific Event types e.g. keyboard_event. + */ +class Event +{ + public: + /** + * Construct a new Quit event. + * + * @param event + * Quit event. + */ + Event(QuitEvent event); + /** + * Construct a new keyboard event. + * + * @param event + * Keyboard event. + */ + Event(KeyboardEvent event); + + /** + * Construct a new mouse event. + * + * @param event + * Mouse event. + */ + Event(MouseEvent event); + + /** + * Construct a new mouse button event. + * + * @param event + * Mouse button event. + */ + Event(MouseButtonEvent event); + + /** + * Construct a new touch event. + * + * @param event + * Touch event. + */ + Event(TouchEvent event); + + /** + * Get type of event. + * + * @returns + * Event type. + */ + EventType type() const; + + /** + * Check if Event is a quit event. + * + * @returns + * True if this Event is a quit event, else false. + */ + bool is_quit() const; + + /** + * Check if Event is a keyboard event. + * + * @returns + * True if Event is a keyboard event, else false. + */ + bool is_key() const; + + /** + * Check if this Event is a specific Key event. + * + * @param key + * Key to check. + * + * @returns + * True if Event is a keyboard Event and matches supplied key. + */ + bool is_key(Key key) const; + + /** + * Check if this Event is a specific Key Event and state. + * + * @param key + * Key to check. + * + * @param state + * State to check. + * + * @returns + * True if Event is a keyboard Event and matches supplied Key and + * state. + */ + bool is_key(Key key, KeyState state) const; + + /** + * Get keyboard event, will throw if wrong type. + * + * @returns + * Keyboard event. + */ + KeyboardEvent key() const; + + /** + * Check if Event is a mouse event. + * + * @returns + * True if Event is a mouse event, else false. + */ + bool is_mouse() const; + + /** + * Get mouse event, will throw if wrong type. + * + * @returns + * Mouse event. + */ + MouseEvent mouse() const; + + /** + * Check if event is a mouse button event. + * + * @returns + * True if Event is a mouse button event, else false. + */ + bool is_mouse_button() const; + + /** + * Check if this event is a specific mouse button event + * + * @param button + * Mouse button to check. + * + * @returns + * True if Event is a mouse button Event and matches supplied button. + */ + bool is_mouse_button(MouseButton button) const; + + /** + * Check if this event is a specific mouse button event and state + * + * @param button + * Mouse button to check. + * + * @param state + * State to check. + * + * @returns + * True if Event is a mouse button Event and matches supplied button and + * state. + */ + bool is_mouse_button(MouseButton button, MouseButtonState state) const; + + /** + * Get mouse button event, will throw if wrong type. + * + * @returns + * Mouse button event. + */ + MouseButtonEvent mouse_button() const; + + /** + * Check if Event is a touch event. + * + * @returns + * True if Event is a touch event, else false. + */ + bool is_touch() const; + + /** + * Get touch event will throw if wrong type. + * + * @returns + * Touch event. + */ + TouchEvent touch() const; + + private: + /** Type of event. */ + EventType type_; + + /** Variant of possible Event types. */ + std::variant event_; +}; + +} diff --git a/include/iris/events/event_type.h b/include/iris/events/event_type.h new file mode 100644 index 00000000..960d9f5c --- /dev/null +++ b/include/iris/events/event_type.h @@ -0,0 +1,26 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** + * Enumeration of possible Event types. + */ +enum class EventType : std::uint32_t +{ + QUIT, + KEYBOARD, + MOUSE, + MOUSE_BUTTON, + TOUCH +}; + +} diff --git a/include/iris/events/keyboard_event.h b/include/iris/events/keyboard_event.h new file mode 100644 index 00000000..e5506e0e --- /dev/null +++ b/include/iris/events/keyboard_event.h @@ -0,0 +1,167 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** Encapsulated whether a Key was a down or up press */ +enum class KeyState : std::uint32_t +{ + DOWN, + UP +}; + +/** Encapsulates all Key presses */ +enum class Key : std::uint32_t +{ + UNKNOWN, + NUM_0, + NUM_1, + NUM_2, + NUM_3, + NUM_4, + NUM_5, + NUM_6, + NUM_7, + NUM_8, + NUM_9, + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T, + U, + V, + W, + X, + Y, + Z, + EQUAL, + MINUS, + RIGHT_BRACKET, + LEFT_BRACKET, + QUOTE, + SEMI_COLON, + BACKSLASH, + COMMA, + SLASH, + PERIOD, + GRAVE, + KEYPAD_DECIMAL, + KEYPAD_MULTIPLY, + KEYPAD_PLUS, + KEYPAD_CLEAR, + KEYPAD_DIVIDE, + KEYPAD_ENTER, + KEYPAD_MINUS, + KEYPAD_EQUALS, + KEYPAD_0, + KEYPAD_1, + KEYPAD_2, + KEYPAD_3, + KEYPAD_4, + KEYPAD_5, + KEYPAD_6, + KEYPAD_7, + KEYPAD_8, + KEYPAD_9, + RETURN, + TAB, + SPACE, + DEL, + ESCAPE, + COMMAND, + SHIFT, + CAPS_LOCK, + OPTION, + CONTROL, + RIGHT_SHIFT, + RIGHT_OPTION, + RIGHT_CONTROL, + FUNCTION, + VOLUME_UP, + VOLUME_DOWN, + MUTE, + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + F13, + F14, + F15, + F16, + F17, + F18, + F19, + F20, + HELP, + HOME, + FORWARD_DELETE, + END, + PAGE_UP, + PAGE_DOWN, + LEFT_ARROW, + RIGHT_ARROW, + DOWN_ARROW, + UP_ARROW, +}; + +/** + * Encapsulated a keyboard event. Stores the Key that was pressed + * as well as if the Key was a down or up press + */ +struct KeyboardEvent +{ + /** + * Constructor + * + * @param k + * The Key that was pressed + * + * @param type + * Down or up press + */ + KeyboardEvent(const Key key, const KeyState state) + : key(key) + , state(state) + { + } + + /** Key that was pressed */ + Key key; + + /** Down or up press */ + KeyState state; +}; + +} diff --git a/include/iris/events/mouse_button_event.h b/include/iris/events/mouse_button_event.h new file mode 100644 index 00000000..06b2638f --- /dev/null +++ b/include/iris/events/mouse_button_event.h @@ -0,0 +1,54 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ +/** Encapsulates whether a mouse button we a down or up press. */ +enum class MouseButtonState : std::uint32_t +{ + DOWN, + UP +}; + +/** Encapsulates mouse buttons. */ +enum class MouseButton : std::uint32_t +{ + LEFT, + RIGHT +}; + +/** + * Encapsulates a mouse button event. + */ +struct MouseButtonEvent +{ + /** + * Construct a new MouseButtonEvent. + * + * + * @param button + * Which mouse button was pressed. + * + * @param state + * Down or up press. + */ + MouseButtonEvent(MouseButton button, MouseButtonState state) + : button(button) + , state(state) + { + } + + /** Mouse button pressed. */ + MouseButton button; + + /* Down or up press. */ + MouseButtonState state; +}; +} diff --git a/include/iris/events/mouse_event.h b/include/iris/events/mouse_event.h new file mode 100644 index 00000000..6a923499 --- /dev/null +++ b/include/iris/events/mouse_event.h @@ -0,0 +1,40 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace iris +{ + +/** + * Encapsulates a mouse event. Stores delta x and y movement from previous + * event. + */ +struct MouseEvent +{ + /** + * Construct a new mouse Event with delta x and y coordinates. + * + * @param delta_x + * The amount the cursor has moved along the x-axis since the last event. + * + * @param delta_y + * The amount the cursor has moved along the y-axis since the last event. + */ + MouseEvent(const float delta_x, const float delta_y) + : delta_x(delta_x) + , delta_y(delta_y) + { + } + + /** Amount cursor has moved along x-axis since last mouse Event */ + float delta_x; + + /** Amount cursor has moved along y-axis since last mouse Event */ + float delta_y; +}; + +} diff --git a/include/iris/events/quit_event.h b/include/iris/events/quit_event.h new file mode 100644 index 00000000..e974c45a --- /dev/null +++ b/include/iris/events/quit_event.h @@ -0,0 +1,19 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace iris +{ + +/** + * An event that signals the program should quit. + */ +struct QuitEvent +{ +}; + +} diff --git a/include/iris/events/touch_event.h b/include/iris/events/touch_event.h new file mode 100644 index 00000000..164f09ce --- /dev/null +++ b/include/iris/events/touch_event.h @@ -0,0 +1,71 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +namespace iris +{ + +/** + * Enumeration of touch event types. + */ +enum class TouchType : std::uint32_t +{ + BEGIN, + MOVE, + END +}; + +/** + * Encapsulates a touch event. Stores screen position of event, type and a + * unique id for the event. + * + * The id will be the same for all related touch events, i.e. a BEGIN, MOVE and + * END event for the same touch will all have the same id. Id's are unique + * amongst all active touches but maybe reused for future events. + */ +struct TouchEvent +{ + /** + * Construct a new touch event. + * + * @param id + * Id for this event. + * + * @param type + * Type of event. + * + * @param x + * x coordinate in screen space of event. + * + * @param y + * y coordinate in screen space of event. + */ + TouchEvent(std::uintptr_t id, TouchType type, float x, float y) + : id(id) + , type(type) + , x(x) + , y(y) + { + } + + /** Id of event. */ + std::uintptr_t id; + + /** Type of event. */ + TouchType type; + + /** x screen coordinate. */ + float x; + + /** y screen coordinate. */ + float y; +}; + +} diff --git a/include/iris/graphics/animation.h b/include/iris/graphics/animation.h new file mode 100644 index 00000000..6cae8d13 --- /dev/null +++ b/include/iris/graphics/animation.h @@ -0,0 +1,124 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include + +#include "core/matrix4.h" +#include "core/quaternion.h" +#include "core/transform.h" +#include "core/vector3.h" +#include "graphics/keyframe.h" + +namespace iris +{ + +/** + * An animation represents a collection of keyframes for bones as well as + * providing an interface for smoothly interpolating through them. An animation + * will loop indefinitely. + */ +class Animation +{ + public: + /** + * Construct a new Animation. + * + * @param duration + * Length of animation. + * + * @param name + * Name of animation, should be unique within a skeleton. + * + * @param frames + * Map of bone names to keyframes. Keyframes must be in time order. + */ + Animation( + std::chrono::milliseconds duration, + const std::string &name, + const std::map> &frames); + + /** + * Get animation name. + * + * @returns + * Animation name. + */ + std::string name() const; + + /** + * Get the Transformation for a bone at the current animation time. If the + * time falls between two keyframes then this method will interpolate + * between them. + * + * @param bone + * Bone name. + * + * @returns + * Transformation of supplied bone at current animation time. + */ + Transform transform(const std::string &bone) const; + + /** + * Check if a bone exists in the animation. + * + * @param bone + * Bone name. + * + * @returns + * True if bone exists, false otherwise. + */ + bool bone_exists(const std::string &bone) const; + + /** + * Advances the animation by the amount of time since the last call. + */ + void advance(); + + /** + * Reset animation time back to 0. + */ + void reset(); + + /** + * Get animation duration. + * + * @returns + * Duration of animation. + */ + std::chrono::milliseconds duration() const; + + /** + * Set the current time of animation, must be in range [0, duration()]. + * + * @param time + * New time. + */ + void set_time(std::chrono::milliseconds time); + + private: + /** Current time of animation. */ + std::chrono::milliseconds time_; + + /** When the animation was last advanced. */ + std::chrono::steady_clock::time_point last_advance_; + + /** Length of the animation. */ + std::chrono::milliseconds duration_; + + /** Name of animation. */ + std::string name_; + + /** Collection of bones and their keyframes. */ + std::map> frames_; +}; + +} diff --git a/include/iris/graphics/bone.h b/include/iris/graphics/bone.h new file mode 100644 index 00000000..f4978b20 --- /dev/null +++ b/include/iris/graphics/bone.h @@ -0,0 +1,148 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "core/matrix4.h" +#include "core/transform.h" +#include "graphics/weight.h" + +namespace iris +{ + +/** + * A bone is a component of skeletal animation. It represents a series of + * vertices it influences as well as the amount it influences them (weight). + * Alternatively a bone may have zero weights, in this case it represents an + * intermediate transformation in the bone hierarchy. + * + * A bone stores two matrices: + * offset - transforms vertices from local space to bone space + * transform - transforms vertices into the current bone pose + * + * During execution the offset is immutable but the transform will change as the + * skeleton the bone is part of is animated. + * + * If a bone is set to "manual" then it's transform is absolute i.e. a Skeleton + * will not apply its parent transformation. This is useful if you want to set + * a position in world space. + */ +class Bone +{ + public: + /** + * Construct a new Bone. + * + * @param name + * Unique name of the bone. + * + * @param parent + * Unique name of the parent bone. + * + * @param weights + * Collection of Weight objects that define the bone. May be empty. + * + * @param offset + * Matrix which transforms vertices from local space to bone space. + * + * @param transform + * Initial matrix which transforms bone for an animation. + */ + Bone( + const std::string &name, + const std::string &parent, + const std::vector &weights, + const Matrix4 &offset, + const Matrix4 &transform); + + /** + * Get name of bone. + * + * @returns + * Bone name. + */ + std::string name() const; + + /** Get name of parent pone. + * + * @returns + * Parent bone name. + */ + std::string parent() const; + + /** + * Get reference to collection of weights. + * + * @returns + * Reference to weights. + */ + const std::vector &weights() const; + + /** + * Get reference to offset matrix. + * + * @returns + * Reference to offset matrix. + */ + const Matrix4 &offset() const; + + /** + * Get reference to transformation matrix. + * + * @returns + * Reference to transform matrix. + */ + const Matrix4 &transform() const; + + /** + * Set the transformation matrix. + * + * @param transform + * New transform. + */ + void set_transform(const Matrix4 &transform); + + /** + * Check if the bone is manual. + * + * @returns + * True if bone is manual, false otherwise. + */ + bool is_manual() const; + + /** + * Set if bone is manual. + * + * @param is_manual + * New manual value + */ + void set_manual(bool is_manual); + + private: + /** Bone name. */ + std::string name_; + + /** Parent bone name. */ + std::string parent_; + + /** Collection of weights. */ + std::vector weights_; + + /** Offset matrix. */ + Matrix4 offset_; + + /** Transformation matrix. */ + Matrix4 transform_; + + /** If bone is manual. */ + bool is_manual_; +}; + +} diff --git a/include/iris/graphics/constant_buffer_writer.h b/include/iris/graphics/constant_buffer_writer.h new file mode 100644 index 00000000..0ee66db1 --- /dev/null +++ b/include/iris/graphics/constant_buffer_writer.h @@ -0,0 +1,95 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +namespace iris +{ + +/** + * This class provides a convenient way of writing to some (graphics api + * specific) constant data buffer. It is designed such that subsequent calls to + * write() will automatically advance, so the buffer can be filled by repeated + * calls. + */ +template +class ConstantBufferWriter +{ + public: + /** + * Construct a new ConstantBufferWriter. + * + * @param buffer + * Buffer to write data to. + */ + ConstantBufferWriter(T &buffer) + : buffer_(buffer) + , offset_(0u) + { + } + + /** + * Write an object to the buffer at the current position. + * + * @param object + * Object to write. + */ + template + void write(const S &object) + { + buffer_.write(object, offset_); + offset_ += sizeof(S); + } + + /** + * Write a collection of objects to the buffer at the current position. + * + * @param objects + * Objects to write. + */ + template + void write(const std::vector &objects) + { + buffer_.write(objects.data(), sizeof(S) * objects.size(), offset_); + offset_ += sizeof(S) * objects.size(); + } + + /** + * Write an array of objects to the buffer at the current position. + * + * @param objects + * Objects to write. + */ + template + void write(const std::array &objects) + { + buffer_.write(objects.data(), sizeof(S) * N, offset_); + offset_ += sizeof(S) * objects.size(); + } + + /** + * Advance the internal offset into the buffer. + * + * @param offset + * Amount (in bytes) to increment internal offset. + */ + void advance(std::size_t offset) + { + offset_ += offset; + } + + private: + /** Buffer to write to, */ + T &buffer_; + + /** Offset into buffer to write to. */ + std::size_t offset_; +}; + +} diff --git a/include/iris/graphics/d3d12/d3d12_buffer.h b/include/iris/graphics/d3d12/d3d12_buffer.h new file mode 100644 index 00000000..f2052bfe --- /dev/null +++ b/include/iris/graphics/d3d12/d3d12_buffer.h @@ -0,0 +1,117 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include + +#include "directx/d3d12.h" + +#include "graphics/vertex_data.h" + +namespace iris +{ + +/** + * This class encapsulates a d3d12 buffer. A buffer can be created with either + * vertex or index data. + * + * Note that this class provides access to d3d12 views to the data stored. These + * are accessed via either vertex_view() or index_view(), however only one of + * these calls will be valid and that is based in which constructor was used. + * This is an internal class and as such the engine knows how to correctly call + * the required view. This class does *not* perform any checks on the view + * class. Calling the incorrect view call is undefined. + */ +class D3D12Buffer +{ + public: + /** + * Construct a new D3D12Buffer with vertex data. + * + * @param vertex_data + * Vertex data to copy to buffer. + */ + D3D12Buffer(const std::vector &vertex_data); + + /** + * Construct a new D3D12Buffer with index data. + * + * @param vertex_data + * Index data to copy to buffer. + */ + D3D12Buffer(const std::vector &index_data); + + D3D12Buffer(const D3D12Buffer &) = delete; + D3D12Buffer &operator=(const D3D12Buffer &) = delete; + + /** + * Get the number of elements stored in the buffer. + * + * @returns + * Number of elements in buffer. + */ + std::size_t element_count() const; + + /** + * Get the native view to the vertex data. Only valid if the vertex data + * constructor was used. + * + * @returns + * D3D12 view to vertex data. + */ + D3D12_VERTEX_BUFFER_VIEW vertex_view() const; + + /** + * Get the native view to the index data. Only valid if the index data + * constructor was used. + * + * @returns + * D3D12 view to index data. + */ + D3D12_INDEX_BUFFER_VIEW index_view() const; + + /** + * Write vertex data to the buffer. + * + * @param vertex_data + * New vertex data. + */ + void write(const std::vector &vertex_data); + + /** + * Write index data to the buffer. + * + * @param index_data + * New index data. + */ + void write(const std::vector &index_data); + + private: + /** D3D12 handle to buffer. */ + ::Microsoft::WRL::ComPtr resource_; + + /** View to vertex data, only valid for vertex data constructor. */ + D3D12_VERTEX_BUFFER_VIEW vertex_buffer_view_; + + /** View to index data, only valid for index data constructor. */ + D3D12_INDEX_BUFFER_VIEW index_buffer_view_; + + /** Number of elements in buffer. */ + std::size_t element_count_; + + /** Maximum number of elements that can be stored in buffer. */ + std::size_t capacity_; + + /** Pointer to mapped memory where new buffer data can be written. */ + std::byte *mapped_memory_; +}; + +} diff --git a/include/iris/graphics/d3d12/d3d12_constant_buffer.h b/include/iris/graphics/d3d12/d3d12_constant_buffer.h new file mode 100644 index 00000000..34693af2 --- /dev/null +++ b/include/iris/graphics/d3d12/d3d12_constant_buffer.h @@ -0,0 +1,110 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +#include "core/exception.h" +#include "graphics/d3d12/d3d12_context.h" +#include "graphics/d3d12/d3d12_cpu_descriptor_handle_allocator.h" +#include "graphics/d3d12/d3d12_descriptor_handle.h" +#include "graphics/d3d12/d3d12_descriptor_manager.h" +#include "graphics/d3d12/d3d12_gpu_descriptor_handle_allocator.h" +#include "graphics/texture_manager.h" + +namespace iris +{ + +/** + * This class encapsulates a constant shader buffer. This is data that is set + * once then made available to all vertices/fragments. It is analogous to an + * OpenGL uniform. + */ +class D3D12ConstantBuffer +{ + public: + /** + * Construct a null D3D12ConstantBuffer. + */ + D3D12ConstantBuffer(); + + /** + * Construct a new D3D12ConstantBuffer. + * + * @param capacity + * Size (in bytes) of buffer. + */ + D3D12ConstantBuffer(std::uint32_t capacity); + + /** + * Get descriptor handle to buffer. + * + * @returns + * Buffer handle. + */ + D3D12DescriptorHandle descriptor_handle() const; + + /** + * Write an object into the buffer at an offset. + * + * @param object + * Object to write. + * + * @param offset + * Offset into buffer to write object. + */ + template + void write(const T &object, std::size_t offset) + { + write(std::addressof(object), sizeof(T), offset); + } + + /** + * Write an object into the buffer at an offset. + * + * @param object + * Object to write. + * + * @param size + * Size (in bytes) of object to write. + * + * @param offset + * Offset into buffer to write object. + */ + template + void write(const T *object, std::size_t size, std::size_t offset) + { + if (offset + size > capacity_) + { + throw Exception("write would overflow"); + } + + std::memcpy(mapped_buffer_ + offset, object, size); + } + + private: + /** Capacity (in bytes) of buffer. */ + std::uint32_t capacity_; + + /** Pointer to mapped buffer where data can be written. */ + std::byte *mapped_buffer_; + + /** D3D12 handle to resource view. */ + Microsoft::WRL::ComPtr resource_; + + /** D3D12 handle to buffer. */ + D3D12DescriptorHandle descriptor_handle_; +}; + +} diff --git a/include/iris/graphics/d3d12/d3d12_constant_buffer_pool.h b/include/iris/graphics/d3d12/d3d12_constant_buffer_pool.h new file mode 100644 index 00000000..89253c36 --- /dev/null +++ b/include/iris/graphics/d3d12/d3d12_constant_buffer_pool.h @@ -0,0 +1,45 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "graphics/d3d12/d3d12_constant_buffer.h" + +namespace iris +{ + +/** + * This class encapsulates a pool of D3D12ConstantBuffer objects as a fixed size + * circular buffer. + */ +class D3D12ConstantBufferPool +{ + public: + /** + * Construct a new D3D12ConstantBufferPool. + */ + D3D12ConstantBufferPool(); + + /** + * Get the next D3D12ConstantBuffer in the circular buffer. + * + * @returns + * Next D3D12ConstantBuffer. + */ + D3D12ConstantBuffer &next(); + + private: + /** Pool of D3D12ConstantBuffer objects. */ + std::vector buffers_; + + /** Index into pool of next free object. */ + std::size_t index_; +}; + +} diff --git a/include/iris/graphics/d3d12/d3d12_context.h b/include/iris/graphics/d3d12/d3d12_context.h new file mode 100644 index 00000000..3313cf9a --- /dev/null +++ b/include/iris/graphics/d3d12/d3d12_context.h @@ -0,0 +1,90 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include +#include + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +namespace iris +{ + +/** + * This class provides singleton access to various D3D12 primitives. + */ +class D3D12Context +{ + public: + /** + * Get the DXGI factory.c + * + * @returns + * DXGI factory. + */ + static IDXGIFactory4 *dxgi_factory(); + + /** + * Get the D3D12 device. + * + * @returns + * D3D12 device. + */ + static ID3D12Device2 *device(); + + /** + * Get the root signature. + * + * @returns + * Root signature. + */ + static ID3D12RootSignature *root_signature(); + + /** + * Get the number of descriptors in a descriptors table. + * + * @returns + * Number of descriptors. + */ + static std::uint32_t num_descriptors(); + + private: + // private to force access through above public static methods + D3D12Context(); + static D3D12Context &instance(); + + // these are the member function equivalents of the above static methods + // see their docs for details + + IDXGIFactory4 *dxgi_factory_impl() const; + + ID3D12Device2 *device_impl() const; + + ID3D12RootSignature *root_signature_impl() const; + + std::uint32_t num_descriptors_impl() const; + + /** D3D12 handle to dxgi factory. */ + Microsoft::WRL::ComPtr dxgi_factory_; + + /** D3D12 handle to d3d12 device. */ + Microsoft::WRL::ComPtr device_; + + /** D3D12 handle to d3d12 info queue. */ + Microsoft::WRL::ComPtr info_queue_; + + /** D3D12 handle to d3d12 root signature. */ + Microsoft::WRL::ComPtr root_signature_; + + /* Number of descriptors in descriptor table. */ + std::uint32_t num_descriptors_; +}; + +} diff --git a/include/iris/graphics/d3d12/d3d12_cpu_descriptor_handle_allocator.h b/include/iris/graphics/d3d12/d3d12_cpu_descriptor_handle_allocator.h new file mode 100644 index 00000000..e5870859 --- /dev/null +++ b/include/iris/graphics/d3d12/d3d12_cpu_descriptor_handle_allocator.h @@ -0,0 +1,141 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +#include "graphics/d3d12/d3d12_descriptor_handle.h" + +namespace iris +{ + +/** + * This class provides the mechanisms for allocating cpu descriptor heaps and + * descriptor handles. An instance of this class will pre-allocate a fixed sized + * pool of descriptors for a given d3d12 heap type, it then provides methods for + * allocating handles from this pool. This class maintains two types of + * allocation: + * - static : allocated for the lifetime of the class + * - dynamic : reserved until reset + * + * This allows for handles to be allocated that only live for one render pass + * (dynamic) or multiple frames (static). + * + * Example heap layout: + * + *i Pool of all descriptor handles + * +----------------------+ -. + * | static descriptor 1 | | + * +----------------------+ | + * | static descriptor 2 | | + * +----------------------+ | + * | | | Static pool + * | | | + * | | | + * | | | + * | | | + * ,- +----------------------+ -' + * | | dynamic descriptor 1 | + * | +----------------------+ + * | | dynamic descriptor 2 | + * | +----------------------+ + * | | dynamic descriptor 3 | + * Dynamic pool | +----------------------+ + * | | | + * | | | + * | | | + * | | | + * | | | + * | | | + * '-+----------------------+ + */ +class D3D12CPUDescriptorHandleAllocator +{ + public: + /** + * Construct a new D3D12CPUDescriptorHandleAllocator. + * + * @param type + * D3D12 heap type to create. + * + * @param num_descriptors + * Number of descriptors to create in heap (static + dynamic). + * + * @param static_size + * The number of descriptors to reserve for static descriptors. + * + * @param shader_visible + * Flag indicating whether the allocated descriptors should be visible to + * the gpu or just the cpu. + */ + D3D12CPUDescriptorHandleAllocator( + D3D12_DESCRIPTOR_HEAP_TYPE type, + std::uint32_t num_descriptors, + std::uint32_t static_size, + bool shader_visible = false); + + /** + * Allocate a static descriptor from the pool. + * + * @returns + * A new static descriptor. + */ + D3D12DescriptorHandle allocate_static(); + + /** + * Allocate a dynamic descriptor from the pool. + * + * @returns + * A new dynamic descriptor. + */ + D3D12DescriptorHandle allocate_dynamic(); + + /** + * Get the size of a descriptor handle. + * + * @returns + * Descriptor handle size. + */ + std::uint32_t descriptor_size() const; + + /** + * Reset the dynamic allocation. This means future calls to allocate_dynamic + * could return handles that have been previously allocated. + */ + void reset_dynamic(); + + private: + /** D3D12 handle to descriptor heap. */ + ::Microsoft::WRL::ComPtr<::ID3D12DescriptorHeap> descriptor_heap_; + + /** D3D12 handle to start of heap. */ + D3D12_CPU_DESCRIPTOR_HANDLE heap_start_; + + /** Size of a single descriptor handle. */ + std::uint32_t descriptor_size_; + + /** The current index into the static pool. */ + std::uint32_t static_index_; + + /** The current index into the dynamic pool. */ + std::uint32_t dynamic_index_; + + /** Maximum number of static handles. */ + std::uint32_t static_capacity_; + + /** Maximum number of dynamic handles. */ + std::uint32_t dynamic_capacity_; +}; + +} diff --git a/include/iris/graphics/d3d12/d3d12_descriptor_handle.h b/include/iris/graphics/d3d12/d3d12_descriptor_handle.h new file mode 100644 index 00000000..d37f50c1 --- /dev/null +++ b/include/iris/graphics/d3d12/d3d12_descriptor_handle.h @@ -0,0 +1,81 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +namespace iris +{ + +/** + * This class encapsulates a d3d12 descriptor handle, which is a unique handle + * to an opaque block of data that fully describes an object to the gpu. + * + * For more information see: + * https://docs.microsoft.com/en-us/windows/win32/direct3d12/descriptors-overview + */ +class D3D12DescriptorHandle +{ + public: + /** + * Construct an empty (or null) D3D12DescriptorHandle. + */ + D3D12DescriptorHandle(); + + /** + * Construct a D3D12DescriptorHandle with a cpu handle. + * + * @param cpu_handle + * CPU handle for desciptor. + */ + D3D12DescriptorHandle(D3D12_CPU_DESCRIPTOR_HANDLE cpu_handle); + + /** + * Construct a D3D12DescriptorHandle with a cpu and gpu handle. + * + * @param cpu_handle + * CPU handle for descriptor. + * + * @param gpu_handle + * GPU handle for descriptor. + */ + D3D12DescriptorHandle(D3D12_CPU_DESCRIPTOR_HANDLE cpu_handle, D3D12_GPU_DESCRIPTOR_HANDLE gpu_handle); + + /** + * Get cpu handle, maybe null. + * + * @returns + * CPU handle. + */ + D3D12_CPU_DESCRIPTOR_HANDLE cpu_handle() const; + + /** + * Get gpu handle, maybe null. + * + * @returns + * GPU handle. + */ + D3D12_GPU_DESCRIPTOR_HANDLE gpu_handle() const; + + /** + * Get if this object has a descriptor. + * + * @returns + * True if object has a descriptor, false otherwise. + */ + explicit operator bool() const; + + private: + /** CPU descriptor handle, maybe null. */ + D3D12_CPU_DESCRIPTOR_HANDLE cpu_handle_; + + /** GPU descriptor handle, maybe null. */ + D3D12_GPU_DESCRIPTOR_HANDLE gpu_handle_; +}; + +} diff --git a/include/iris/graphics/d3d12/d3d12_descriptor_manager.h b/include/iris/graphics/d3d12/d3d12_descriptor_manager.h new file mode 100644 index 00000000..a978863c --- /dev/null +++ b/include/iris/graphics/d3d12/d3d12_descriptor_manager.h @@ -0,0 +1,68 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +#include "graphics/d3d12/d3d12_cpu_descriptor_handle_allocator.h" +#include "graphics/d3d12/d3d12_gpu_descriptor_handle_allocator.h" + +namespace iris +{ + +/** + * This class provides singleton access to heap allocators. It manages a + * separate heap for various different d3d12 heap types. + */ +class D3D12DescriptorManager +{ + public: + /** + * Get the cpu allocator for a given heap type. + * + * @param type + * D3D12 heap type to get allocator for. + * + * @returns + * Heap allocator for provided type. + */ + static D3D12CPUDescriptorHandleAllocator &cpu_allocator(D3D12_DESCRIPTOR_HEAP_TYPE type); + + /** + * Get the gpu allocator for a given heap type. + * + * @param type + * D3D12 heap type to get allocator for. + * + * @returns + * Heap allocator for provided type. + */ + static D3D12GPUDescriptorHandleAllocator &gpu_allocator(D3D12_DESCRIPTOR_HEAP_TYPE type); + + private: + // private to force access through above public static methods + D3D12DescriptorManager(); + static D3D12DescriptorManager &instance(); + + // these are the member function equivalents of the above static methods + // see their docs for details + + D3D12CPUDescriptorHandleAllocator &cpu_allocator_impl(D3D12_DESCRIPTOR_HEAP_TYPE type); + + D3D12GPUDescriptorHandleAllocator &gpu_allocator_impl(D3D12_DESCRIPTOR_HEAP_TYPE type); + + /** Map of D3D12 heap type to heap allocator. */ + std::unordered_map cpu_allocators_; + + /** Map of D3D12 heap type to heap allocator. */ + std::unordered_map gpu_allocators_; +}; + +} diff --git a/include/iris/graphics/d3d12/d3d12_gpu_descriptor_handle_allocator.h b/include/iris/graphics/d3d12/d3d12_gpu_descriptor_handle_allocator.h new file mode 100644 index 00000000..41330038 --- /dev/null +++ b/include/iris/graphics/d3d12/d3d12_gpu_descriptor_handle_allocator.h @@ -0,0 +1,112 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +#include "graphics/d3d12/d3d12_descriptor_handle.h" + +namespace iris +{ + +/** + * This class provides the mechanisms for allocating gpu descriptor heaps + * and descriptor handles. An instance of this class will pre-allocate a + * fixed sized pool of descriptors for a given d3d12 heap type, it then + * provides methods for allocating handles from this pool. + */ +class D3D12GPUDescriptorHandleAllocator +{ + public: + /** + * Construct a new D3D12GPUDescriptorHandleAllocator. + * + * @param type + * D3D12 heap type to create. + * + * @param num_descriptors + * Number of descriptors to create in heap (static + dynamic). + * + */ + D3D12GPUDescriptorHandleAllocator(D3D12_DESCRIPTOR_HEAP_TYPE type, std::uint32_t num_descriptors); + + /** + * Allocate a contiguous range of descriptor handles. + * + * @param count + * Number of handles to allocate. + * + * @returns + * Handle to to first allocated handle in range. + */ + D3D12DescriptorHandle allocate(std::uint32_t count); + + /** + * Get cpu handle of first descriptor in heap. + * + * @returns + * First cpu descriptor in heap. + */ + D3D12_CPU_DESCRIPTOR_HANDLE cpu_start() const; + + /** + * Get gpu handle of first descriptor in heap. + * + * @returns + * First gpu descriptor in heap. + */ + D3D12_GPU_DESCRIPTOR_HANDLE gpu_start() const; + + /** + * Get the size of a descriptor handle. + * + * @returns + * Descriptor handle size. + */ + std::uint32_t descriptor_size() const; + + /** + * Get native handle to heap. + * + * @returns + * Pointer to heap. + */ + ID3D12DescriptorHeap *heap() const; + + /** + * Reset the allocation. This means future calls to allocate could + * return handles that have been previously allocated. + */ + void reset(); + + private: + /** D3D12 handle to descriptor heap. */ + ::Microsoft::WRL::ComPtr<::ID3D12DescriptorHeap> descriptor_heap_; + + /** First cpu handle. */ + D3D12_CPU_DESCRIPTOR_HANDLE cpu_start_; + + /** First gpu handle. */ + D3D12_GPU_DESCRIPTOR_HANDLE gpu_start_; + + /** Size of a single descriptor handle. */ + std::uint32_t descriptor_size_; + + /** Current index into the pool. */ + std::uint32_t index_; + + /** Maximum number of handles. */ + std::uint32_t capacity_; +}; +} diff --git a/include/iris/graphics/d3d12/d3d12_material.h b/include/iris/graphics/d3d12/d3d12_material.h new file mode 100644 index 00000000..f1e96856 --- /dev/null +++ b/include/iris/graphics/d3d12/d3d12_material.h @@ -0,0 +1,86 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +#include "graphics/lights/light_type.h" +#include "graphics/lights/lighting_rig.h" +#include "graphics/material.h" +#include "graphics/primitive_type.h" +#include "graphics/render_graph/render_graph.h" +#include "graphics/texture.h" + +namespace iris +{ + +/** + * Implementation of Material for d3d12. + */ +class D3D12Material : public Material +{ + public: + /** + * Construct a new D3D12Material. + * + * @param render_graph + * RenderGraph describing material. + * + * @param mesh + * Mesh material will be applied to. + * + * @param input_descriptors + * D3D12 vertex descriptor describing how to organise vertex data. + * + * @param light_type + * Type of light for this material. + * + * @param render_to_swapchain + * True if material will be rendered to the swapchain, false otherwise + * (render target). + */ + D3D12Material( + const RenderGraph *render_graph, + const std::vector &input_descriptors, + PrimitiveType primitive_type, + LightType light_type, + bool render_to_swapchain); + + ~D3D12Material() override = default; + + /** + * Get Textures used in this material. + * + * @returns + * Textures used. + */ + std::vector textures() const override; + + /** + * Get the d3d12 pipeline state for this material. + * + * @returns + * Pipeline state. + */ + ID3D12PipelineState *pso() const; + + private: + /** Pipeline state object. */ + ::Microsoft::WRL::ComPtr pso_; + + /** Collection of textures used. */ + std::vector textures_; +}; + +} diff --git a/include/iris/graphics/d3d12/d3d12_mesh.h b/include/iris/graphics/d3d12/d3d12_mesh.h new file mode 100644 index 00000000..ff83c597 --- /dev/null +++ b/include/iris/graphics/d3d12/d3d12_mesh.h @@ -0,0 +1,98 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +#include "graphics/d3d12/d3d12_buffer.h" +#include "graphics/mesh.h" +#include "graphics/vertex_data.h" + +namespace iris +{ + +/** + * Implementation of d3d12 for metal. + */ +class D3D12Mesh : public Mesh +{ + public: + /** + * Construct a new D3D12Mesh. + * + * @param vertices + * Vertices for the mesh. + * + * @param indices + * Indices for the mesh. + * + * @param attributes + * Attributes of the vertices. + */ + D3D12Mesh( + const std::vector &vertices, + const std::vector &indices, + const VertexAttributes &attributes); + + ~D3D12Mesh() override = default; + + /** + * Update the vertex data, this will also update any GPU data. + * + * @param data + * New vertex data. + */ + void update_vertex_data(const std::vector &data) override; + + /** + * Update the index data, this will also update any GPU data. + * + * @param data + * New index data. + */ + void update_index_data(const std::vector &data) override; + + /** + * Get vertex buffer. + * + * @returns + * Const reference to vertex buffer object. + */ + const D3D12Buffer &vertex_buffer() const; + + /** + * Get index buffer. + * + * @returns + * Const reference to index buffer object. + */ + const D3D12Buffer &index_buffer() const; + + /** + * Get d3d12 object which describes vertex layout. + * + * @param + * D3D12 object describing vertex. + */ + std::vector input_descriptors() const; + + private: + /** Buffer for vertex data. */ + D3D12Buffer vertex_buffer_; + + /** Buffer for index data. */ + D3D12Buffer index_buffer_; + + /** D3D12 object describing vertex layout. */ + std::vector input_descriptors_; +}; + +} diff --git a/include/iris/graphics/d3d12/d3d12_mesh_manager.h b/include/iris/graphics/d3d12/d3d12_mesh_manager.h new file mode 100644 index 00000000..d726627d --- /dev/null +++ b/include/iris/graphics/d3d12/d3d12_mesh_manager.h @@ -0,0 +1,45 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "graphics/mesh.h" +#include "graphics/mesh_manager.h" +#include "graphics/vertex_data.h" + +namespace iris +{ + +/** + * Implementation of MeshManager for d3d12. + */ +class D3D12MeshManager : public MeshManager +{ + public: + ~D3D12MeshManager() override = default; + + protected: + /** + * Create a Mesh object from the provided vertex and index data. + * + * @param vertices + * Collection of vertices for the Mesh. + * + * @param indices + * Collection of indices for the Mesh. + * + * @returns + * Loaded Mesh. + */ + std::unique_ptr create_mesh( + const std::vector &vertices, + const std::vector &indices) const override; +}; + +} diff --git a/include/iris/graphics/d3d12/d3d12_render_target.h b/include/iris/graphics/d3d12/d3d12_render_target.h new file mode 100644 index 00000000..b901b1bf --- /dev/null +++ b/include/iris/graphics/d3d12/d3d12_render_target.h @@ -0,0 +1,50 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/d3d12/d3d12_descriptor_handle.h" +#include "graphics/d3d12/d3d12_texture.h" +#include "graphics/render_target.h" + +namespace iris +{ + +/** + * Implementation of RenderTarget for d3d12. + */ +class D3D12RenderTarget : public RenderTarget +{ + public: + /** + * Construct a new D3D12RenderTarget. + * + * @param colour_texture + * Texture to render colour data to. + * + * @param depth_texture + * Texture to render depth data to. + */ + D3D12RenderTarget(std::unique_ptr colour_texture, std::unique_ptr depth_texture); + + ~D3D12RenderTarget() override = default; + + /** + * Get descriptor handle to render target. + * + * @returns + * D3D12 descriptor handle. + */ + D3D12DescriptorHandle handle() const; + + private: + /** D3D12 descriptor handle. */ + D3D12DescriptorHandle handle_; +}; + +} diff --git a/include/iris/graphics/d3d12/d3d12_renderer.h b/include/iris/graphics/d3d12/d3d12_renderer.h new file mode 100644 index 00000000..18ac74df --- /dev/null +++ b/include/iris/graphics/d3d12/d3d12_renderer.h @@ -0,0 +1,191 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +#include "core/auto_release.h" +#include "graphics/d3d12/d3d12_constant_buffer.h" +#include "graphics/d3d12/d3d12_constant_buffer_pool.h" +#include "graphics/d3d12/d3d12_descriptor_handle.h" +#include "graphics/d3d12/d3d12_material.h" +#include "graphics/d3d12/d3d12_texture.h" +#include "graphics/render_target.h" +#include "graphics/renderer.h" + +namespace iris +{ + +/** + * Implementation of Renderer for d3d12. + * + * This Renderer uses triple buffering to allow for greatest rendering + * throughput. A frame if defined as all rendering passes that occur when + * render() is called. This class uses a circular buffer of three frames. When + * all the CPU processing of a frame is complete it is submitted to the GPU. At + * this point the CPU is free to proceed to the next frame whilst the GPU works + * asynchronously. + */ +class D3D12Renderer : public Renderer +{ + public: + /** + * Construct a new D3D12Renderer. + * + * @param window + * The window to present to. + * + * @param width + * Width of window being rendered to. + * + * @param height + * Height of window being rendered to. + * + * @param initial_screen_scale + * The natural scale of the screen with window is currently on. + */ + D3D12Renderer(HWND window, std::uint32_t width, std::uint32_t height, std::uint32_t initial_screen_scale); + + /** + * Destructor, will block until all inflight frames have finished rendering. + */ + ~D3D12Renderer() override; + + /** + * Set the render passes. These will be executed when render() is called. + * + * @param render_passes + * Collection of RenderPass objects to render. + */ + void set_render_passes(const std::vector &render_passes) override; + + /** + * Create a RenderTarget with custom dimensions. + * + * @param width + * Width of render target. + * + * @param height + * Height of render target. + * + * @returns + * RenderTarget. + */ + RenderTarget *create_render_target(std::uint32_t width, std::uint32_t height) override; + + protected: + // handlers for the supported RenderCommandTypes + + void pre_render() override; + void execute_upload_texture(RenderCommand &command) override; + void execute_pass_start(RenderCommand &command) override; + void execute_draw(RenderCommand &command) override; + void execute_pass_end(RenderCommand &command) override; + void execute_present(RenderCommand &command) override; + + private: + /** + * Internal struct encapsulating data needed for a frame. + */ + struct Frame + { + Frame( + Microsoft::WRL::ComPtr buffer, + D3D12DescriptorHandle render_target, + std::unique_ptr depth_buffer, + Microsoft::WRL::ComPtr command_allocator, + Microsoft::WRL::ComPtr fence, + HANDLE fence_event) + : buffer(buffer) + , render_target(render_target) + , depth_buffer(std::move(depth_buffer)) + , command_allocator(command_allocator) + , fence(fence) + , fence_event(fence_event, ::CloseHandle) + { + } + + /** D3D12 back buffer to render to and present. */ + Microsoft::WRL::ComPtr buffer; + + /** render target view for buffer. */ + D3D12DescriptorHandle render_target; + + /** Depth buffer for frame. */ + std::unique_ptr depth_buffer; + + /** Command allocate for frame. */ + Microsoft::WRL::ComPtr command_allocator; + + /** Fence for signaling frame completion. */ + Microsoft::WRL::ComPtr fence; + + /** + * Event to signal frame completion, when event is set then frame is + * safe to use. + */ + AutoRelease fence_event; + + /** Map of RenderCommand objects to constant data buffer pools. */ + std::unordered_map constant_data_buffers; + }; + + /** Width of window to present to. */ + std::uint32_t width_; + + /** Height of window to present to. */ + std::uint32_t height_; + + /** Collection of frames for triple buffering. */ + std::vector frames_; + + /** Index of current frame to render to. */ + std::uint32_t frame_index_; + + /** Single command queue for all frames. */ + Microsoft::WRL::ComPtr command_queue_; + + /** Single command list for all frames. */ + Microsoft::WRL::ComPtr command_list_; + + /** Swap chain for triple buffering. */ + Microsoft::WRL::ComPtr swap_chain_; + + /** Special null buffer to use as padding in table descriptors. */ + std::unique_ptr null_buffer_; + + /** Viewport for window. */ + CD3DX12_VIEWPORT viewport_; + + /** Scissor rect for window. */ + CD3DX12_RECT scissor_rect_; + + /** Collection of created RenderTarget objects. */ + std::vector> render_targets_; + + /** This collection stores materials per light type per render graph. */ + std::unordered_map>> materials_; + + /** Collection of textures that have been uploaded. */ + std::set uploaded_; +}; +} diff --git a/include/iris/graphics/d3d12/d3d12_texture.h b/include/iris/graphics/d3d12/d3d12_texture.h new file mode 100644 index 00000000..87cd31c3 --- /dev/null +++ b/include/iris/graphics/d3d12/d3d12_texture.h @@ -0,0 +1,126 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +#include "core/data_buffer.h" +#include "graphics/d3d12/d3d12_descriptor_handle.h" +#include "graphics/texture.h" +#include "graphics/texture_usage.h" + +namespace iris +{ + +/** + * Implementation of Texture for d3d12. + * + * Internally texture data is first copied to an upload heap. The renderer can + * then encode a command to copy that data to a shader visible heap. + */ +class D3D12Texture : public Texture +{ + public: + /** + * Construct a new D3D12Texture. + * + * @param data + * Raw data of image, in pixel_format. + * + * @param width + * Width of image. + * + * @param height + * Height of image. + * + * @param usage + * Texture usage. + */ + D3D12Texture(const DataBuffer &data, std::uint32_t width, std::uint32_t height, TextureUsage usage); + + ~D3D12Texture() override = default; + + /* + * Get the handle to the d3d12 resource where the image data will be copied + * to. + * + * @returns + * D3D12 resource. + */ + ID3D12Resource *resource() const; + + /** + * Get the handle to the d3d12 resource where the image data is initially + * uploaded to + * + * @returns + * D3D12 resource. + */ + ID3D12Resource *upload() const; + + /** + * Get the d3d12 footprint describing the image data layout. + * + * @returns + * D3D12 footprint. + */ + D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint() const; + + /** + * Get the handle to the image resource view. Only valid if the object was + * constructed for non-depth buffer usage. + * + * @returns + * Resource view handle. + */ + D3D12DescriptorHandle handle() const; + + /** + * Get the handle to the image resource view. Only valid if the object was + * constructed for depth buffer usage. + * + * @returns + * Resource view handle (depth buffer only). + */ + D3D12DescriptorHandle depth_handle() const; + + /** + * Get the type of heap image data will be copied to. + * + * @returns + * Heap type. + */ + D3D12_DESCRIPTOR_HEAP_TYPE type() const; + + private: + /** Handle to resource where image data will be coped to. */ + Microsoft::WRL::ComPtr resource_; + + /** Handle to resource where image data is uploaded to. */ + Microsoft::WRL::ComPtr upload_; + + /** Resource view to image data. */ + D3D12DescriptorHandle resource_view_; + + /** Resource view to image data (depth only). */ + D3D12DescriptorHandle depth_resource_view_; + + /** Footprint describing image data layout. */ + D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprint_; + + /** Type of heap to copy data to. */ + D3D12_DESCRIPTOR_HEAP_TYPE type_; +}; + +} diff --git a/include/iris/graphics/d3d12/d3d12_texture_manager.h b/include/iris/graphics/d3d12/d3d12_texture_manager.h new file mode 100644 index 00000000..6d6f4ce8 --- /dev/null +++ b/include/iris/graphics/d3d12/d3d12_texture_manager.h @@ -0,0 +1,51 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "core/data_buffer.h" +#include "graphics/texture.h" +#include "graphics/texture_manager.h" +#include "graphics/texture_usage.h" + +namespace iris +{ + +/** + * Implementation of TextureManager for d3d12. + */ +class D3D12TextureManager : public TextureManager +{ + public: + ~D3D12TextureManager() override = default; + + protected: + /** + * Create a Texture object with the provided data. + * + * @param data + * Raw data of image, in pixel_format. + * + * @param width + * Width of image. + * + * @param height + * Height of image. + * + * @param usage + * Usage of the texture. + */ + std::unique_ptr do_create( + const DataBuffer &data, + std::uint32_t width, + std::uint32_t height, + TextureUsage usage) override; +}; + +} diff --git a/include/iris/graphics/d3d12/hlsl_shader_compiler.h b/include/iris/graphics/d3d12/hlsl_shader_compiler.h new file mode 100644 index 00000000..97f74ad7 --- /dev/null +++ b/include/iris/graphics/d3d12/hlsl_shader_compiler.h @@ -0,0 +1,132 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +#include "core/colour.h" +#include "core/vector3.h" +#include "graphics/lights/light_type.h" +#include "graphics/render_graph/render_graph.h" +#include "graphics/render_graph/shader_compiler.h" +#include "graphics/texture.h" +#include "graphics/vertex_attributes.h" + +namespace iris +{ + +class RenderNode; +class PostProcessingNode; +class ColourNode; +class TextureNode; +class InvertNode; +class BlurNode; +class CompositeNode; +class VertexPositionNode; +class ArithmeticNode; +class ConditionalNode; +class ComponentNode; +class CombineNode; +class SinNode; +template +class ValueNode; + +/** + * Implementation of ShaderCompiler for HLSL. + */ +class HLSLShaderCompiler : public ShaderCompiler +{ + public: + /** + * Construct a new HLSLSHaderCompiler. + * + * @param render_graph + * RenderGraph to compile into HLSL. + * + * @param light_type + * The type of light to render with. + */ + HLSLShaderCompiler(const RenderGraph *render_graph, LightType light_type); + + ~HLSLShaderCompiler() override = default; + + // visitor methods + void visit(const RenderNode &node) override; + void visit(const PostProcessingNode &node) override; + void visit(const ColourNode &node) override; + void visit(const TextureNode &node) override; + void visit(const InvertNode &node) override; + void visit(const BlurNode &node) override; + void visit(const CompositeNode &node) override; + void visit(const VertexPositionNode &node) override; + void visit(const ValueNode &node) override; + void visit(const ValueNode &node) override; + void visit(const ValueNode &node) override; + void visit(const ArithmeticNode &node) override; + void visit(const ConditionalNode &node) override; + void visit(const ComponentNode &node) override; + void visit(const CombineNode &node) override; + void visit(const SinNode &node) override; + + /** + * Get the compiled vertex shader. + * + * Compiled here means from the render graph to a string, not to an API + * specific object on the hardware. + * + * @returns + * Vertex shader. + */ + std::string vertex_shader() const override; + + /** + * Get the compiled fragment shader. + * + * Compiled here means from the render graph to a string, not to an API + * specific object on the hardware. + * + * @returns + * Fragment shader. + */ + std::string fragment_shader() const override; + + /** + * Collection of textures needed for the shaders. + * + * @returns + * Collection of Textures. + */ + std::vector textures() const override; + + private: + /** Stream for vertex shader. */ + std::stringstream vertex_stream_; + + /** Stream for fragment shader. */ + std::stringstream fragment_stream_; + + /** Pointer to the current shader stream. */ + std::stringstream *current_stream_; + + /** Collection of vertex functions. */ + std::set vertex_functions_; + + /** Collection of fragment functions. */ + std::set fragment_functions_; + + /** Pointer to current function collection. */ + std::set *current_functions_; + + /** Textures needed for shaders. */ + std::vector textures_; + + LightType light_type_; +}; +} diff --git a/include/iris/graphics/ios/app_delegate.h b/include/iris/graphics/ios/app_delegate.h new file mode 100644 index 00000000..6258440b --- /dev/null +++ b/include/iris/graphics/ios/app_delegate.h @@ -0,0 +1,19 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#import + +@interface AppDelegate : UIResponder + +/** Main window. */ +@property(strong, nonatomic) UIWindow *window; + +/** + * Calls entry into game. + */ +- (void)callEntry; + +@end diff --git a/include/iris/graphics/ios/ios_window.h b/include/iris/graphics/ios/ios_window.h new file mode 100644 index 00000000..8514cc0d --- /dev/null +++ b/include/iris/graphics/ios/ios_window.h @@ -0,0 +1,54 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/window.h" + +namespace iris +{ + +/** + * Implementation of Window for iOS. + */ +class IOSWindow : public Window +{ + public: + /** + * Construct a new IOSWindow. + * + * @param width + * Width of window. + * + * @param height + * Height of window. + */ + IOSWindow(std::uint32_t width, std::uint32_t height); + ~IOSWindow() override = default; + + /** + * Get the natural scale for the screen. This value reflects the scale + * factor needed to convert from the default logical coordinate space into + * the device coordinate space of this screen. + * + * @returns + * Screen scale factor. + */ + std::uint32_t screen_scale() const override; + + /** + * Pump the next user input event. Result will be empty if there are no + * new events. + * + * @returns + * Optional event. + */ + std::optional pump_event() override; +}; + +} diff --git a/include/iris/graphics/ios/ios_window_manager.h b/include/iris/graphics/ios/ios_window_manager.h new file mode 100644 index 00000000..badc25e7 --- /dev/null +++ b/include/iris/graphics/ios/ios_window_manager.h @@ -0,0 +1,50 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "graphics/window.h" +#include "graphics/window_manager.h" + +namespace iris +{ + +/** + * Implementation of WindowManager for ios. + */ +class IOSWindowManager : public WindowManager +{ + public: + ~IOSWindowManager() override = default; + + /** + * Create a new Window. + * + * @param width + * Width of window. + * + * @param height + * Height of window. + */ + Window *create_window(std::uint32_t width, std::uint32_t height) override; + + /** + * Get the currently active window. + * + * @returns + * Pointer to current window, nullptr if one does not exist. + */ + Window *current_window() const override; + + private: + /** Current window .*/ + std::unique_ptr current_window_; +}; + +} diff --git a/include/iris/graphics/ios/metal_view.h b/include/iris/graphics/ios/metal_view.h new file mode 100644 index 00000000..aa5c26bc --- /dev/null +++ b/include/iris/graphics/ios/metal_view.h @@ -0,0 +1,14 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#import + +@interface MetalView : UIView + +@property(nonatomic, strong) id device; +@property(nonatomic, weak) CAMetalLayer *metalLayer; + +@end diff --git a/include/iris/graphics/ios/metal_view_controller.h b/include/iris/graphics/ios/metal_view_controller.h new file mode 100644 index 00000000..506d4c01 --- /dev/null +++ b/include/iris/graphics/ios/metal_view_controller.h @@ -0,0 +1,22 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#import + +#include + +#include + +#include "events/event.h" + +@interface MetalViewController : UIViewController +{ + + @public + std::queue events_; +} + +@end diff --git a/include/iris/graphics/keyframe.h b/include/iris/graphics/keyframe.h new file mode 100644 index 00000000..ba1d0273 --- /dev/null +++ b/include/iris/graphics/keyframe.h @@ -0,0 +1,43 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "core/transform.h" + +namespace iris +{ + +/** + * A KeyFrame represents a transformation at a given time in an animation. + */ +struct KeyFrame +{ + /** + * Construct a new KeyFrame. + * + * @param transform + * Transform at time. + * + * @param time + * Time of keyframe (since start of animation). + */ + KeyFrame(const Transform &transform, std::chrono::milliseconds time) + : transform(transform) + , time(time) + { + } + + /** Transform of frame. */ + Transform transform; + + /** Time of frame. */ + std::chrono::milliseconds time; +}; + +} diff --git a/include/iris/graphics/lights/ambient_light.h b/include/iris/graphics/lights/ambient_light.h new file mode 100644 index 00000000..49c8a7f4 --- /dev/null +++ b/include/iris/graphics/lights/ambient_light.h @@ -0,0 +1,88 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "core/colour.h" +#include "graphics/lights/light.h" +#include "graphics/lights/light_type.h" + +namespace iris +{ + +/** + * Implementation of Light for a constant and uniform light without direction or + * position. + */ +class AmbientLight : public Light +{ + public: + /** + * Construct a new AmbientLight. + * + * @param colour + * The colour of the light. + */ + AmbientLight(const Colour &colour); + + ~AmbientLight() override = default; + + /** + * Get the type of light. + * + * @returns + * Light type. + */ + LightType type() const override; + + /** + * Get the raw data for the light colour. + * + * @returns + * Raw data (as floats) for the colour. + */ + std::array colour_data() const override; + + /** + * Unused by this light type. + * + * @returns + * Array of 0.0f values. + */ + std::array world_space_data() const override; + + /** + * Unused by this light type. + * + * @returns + * Array of 0.0f values. + */ + std::array attenuation_data() const override; + + /** + * Get the colour of the light. + * + * @param + * Light colour. + */ + Colour colour() const; + + /** + * Set the colour of the light. + * + * @param + * New light colour. + */ + void set_colour(const Colour &colour); + + private: + /** Colour of ambient light. */ + Colour colour_; +}; + +} diff --git a/include/iris/graphics/lights/directional_light.h b/include/iris/graphics/lights/directional_light.h new file mode 100644 index 00000000..e67b90d6 --- /dev/null +++ b/include/iris/graphics/lights/directional_light.h @@ -0,0 +1,120 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "core/camera.h" +#include "core/vector3.h" +#include "graphics/lights/light.h" + +namespace iris +{ + +/** + * An implementation of Light for a directional light. This is a light + * infinitely far away from the scene and consistent in all directions. + * + * A light may be constructed to cast shadows, this will cause extra render + * passes to be created which can impact performance (depending on scene + * complexity). + */ +class DirectionalLight : public Light +{ + public: + /** Create a new DirectionalLight. + * + * @param direction + * The direction the rays of light are pointing, for examples to have a + * light shining directly down on a scene then its direction would be + * (0, -1, 0). + * + * @param cast_shadows + * True if this light should generate shadows, false otherwise. + */ + DirectionalLight(const Vector3 &direction, bool cast_shadows = false); + + ~DirectionalLight() override = default; + + /** + * Get the type of light. + * + * @returns + * Light type. + */ + LightType type() const override; + + /** + * Unused by this light type. + * + * @returns + * Array of 1.0f values. + */ + std::array colour_data() const override; + + /** + * Get the raw data for the lights world space property i.e direction. + * + * @returns + * Raw data (as floats) for the world space property. + */ + std::array world_space_data() const override; + + /** + * Unused by this light type. + * + * @returns + * Array of 0.0f values. + */ + std::array attenuation_data() const override; + + /** + * Get direction of light. + * + * @returns + * Light direction. + */ + Vector3 direction() const; + + /** + * Set direction of light. + * + * @param direction + * New light direction. + */ + void set_direction(const Vector3 &direction); + + /** + * Check if this light should cast shadows. + * + * @returns + * True if light casts shadows, false otherwise. + */ + bool casts_shadows() const; + + /** + * Get the camera used for rendering the shadow map for this light. + * + * This is used internally and should not normally be called manually. + * + * @returns + * Shadow map camera. + */ + const Camera &shadow_camera() const; + + private: + /** Light direction. */ + Vector3 direction_; + + /** Shadow map camera. */ + Camera shadow_camera_; + + /** Should shadows be generated. */ + bool cast_shadows_; +}; + +} diff --git a/include/iris/graphics/lights/light.h b/include/iris/graphics/lights/light.h new file mode 100644 index 00000000..be50ed31 --- /dev/null +++ b/include/iris/graphics/lights/light.h @@ -0,0 +1,63 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/lights/light_type.h" + +namespace iris +{ + +/** + * Interface for a Light - something which provides luminance in a rendered + * scene. + * + * Whilst this interface defines methods for getting various properties, they + * may not all be valid for all light types. In that case the return value + * should be considered unspecified. + */ +class Light +{ + public: + virtual ~Light() = default; + + /** + * Get the type of light. + * + * @returns + * Light type. + */ + virtual LightType type() const = 0; + + /** + * Get the raw data for the light colour. + * + * @returns + * Raw data (as floats) for the colour. + */ + virtual std::array colour_data() const = 0; + + /** + * Get the raw data for the lights world space property e.g. position or + * direction. + * + * @returns + * Raw data (as floats) for the world space property. + */ + virtual std::array world_space_data() const = 0; + + /** + * Get the raw data for the lights attenuation. + * + * @returns + * Raw data (as floats) for the attenuation. + */ + virtual std::array attenuation_data() const = 0; +}; + +} diff --git a/include/iris/graphics/lights/light_type.h b/include/iris/graphics/lights/light_type.h new file mode 100644 index 00000000..90c10011 --- /dev/null +++ b/include/iris/graphics/lights/light_type.h @@ -0,0 +1,24 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** + * Enumeration of possible light types. + */ +enum class LightType : std::uint8_t +{ + AMBIENT, + DIRECTIONAL, + POINT +}; + +} diff --git a/include/iris/graphics/lights/lighting_rig.h b/include/iris/graphics/lights/lighting_rig.h new file mode 100644 index 00000000..5873010c --- /dev/null +++ b/include/iris/graphics/lights/lighting_rig.h @@ -0,0 +1,35 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "core/colour.h" +#include "graphics/lights/ambient_light.h" +#include "graphics/lights/directional_light.h" +#include "graphics/lights/point_light.h" + +#include +#include + +namespace iris +{ + +/** + * Encapsulates all the lights for a scene. + */ +struct LightingRig +{ + /* Collection of point lights. */ + std::vector> point_lights; + + /** Collection of directional lights. */ + std::vector> directional_lights; + + /** Ambient light colour. */ + std::unique_ptr ambient_light; +}; + +} diff --git a/include/iris/graphics/lights/point_light.h b/include/iris/graphics/lights/point_light.h new file mode 100644 index 00000000..53aca300 --- /dev/null +++ b/include/iris/graphics/lights/point_light.h @@ -0,0 +1,185 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "core/colour.h" +#include "core/vector3.h" +#include "graphics/lights/light.h" +#include "graphics/lights/light_type.h" + +namespace iris +{ + +/** + * Implementation of Light for a light emitting uniformly from a point in 3D + * space. + * + * For this light attenuation is calculated as: + * 1.0 / (constant + (linear * d) + (quadratic * d * d)) + * + * WHere d is the distance of the fragment to the light source. + */ +class PointLight : public Light +{ + public: + /** + * Create a new white PointLight. + * + * @param position + * Position of light in 3D space. + */ + PointLight(const Vector3 &position); + + /** + * Create a new PointLight. + * + * @param position + * Position of light in 3D space. + * + * @param colour + * Colour of the light. + */ + PointLight(const Vector3 &position, const Colour &colour); + + ~PointLight() override = default; + + /** + * Get the type of light. + * + * @returns + * Light type. + */ + LightType type() const override; + + /** + * Get the raw data for the light colour. + * + * @returns + * Raw data (as floats) for the colour. + */ + std::array colour_data() const override; + + /** + * Get the raw data for the lights world space property i.e position. + * + * @returns + * Raw data (as floats) for the world space property. + */ + std::array world_space_data() const override; + + /** + * Get the raw data for the lights attenuation. + * + * @returns + * Raw data (as floats) for the attenuation. + */ + std::array attenuation_data() const override; + + /** + * Get position of light. + * + * @returns + * Light position. + */ + Vector3 position() const; + + /** + * Set light position. + * + * @param position + * New position. + */ + void set_position(const Vector3 &position); + + /** + * Get light colour. + * + * @returns + * Light colour. + */ + Colour colour() const; + + /** + * Set light colour. + * + * @param colour + * New colour. + */ + void set_colour(const Colour &colour); + + /** + * Get constant attenuation term. + * + * @returns + * Constant attenuation term. + */ + float attenuation_constant_term() const; + + /** + * Set constant attenuation term. + * + * @param constant + * New constant term. + */ + void set_attenuation_constant_term(float constant); + + /** + * Get linear attenuation term. + * + * @returns + * Linear attenuation term. + */ + float attenuation_linear_term() const; + + /** + * Set linear attenuation term. + * + * @param linear + * New linear term. + */ + void set_attenuation_linear_term(float linear); + + /** + * Get quadratic attenuation term. + * + * @returns + * Quadratic attenuation term. + */ + float attenuation_quadratic_term() const; + + /** + * Set quadratic attenuation term. + * + * @param quadratic + * New quadratic term. + */ + void set_attenuation_quadratic_term(float quadratic); + + private: + /** + * Struct storing all attenuation terms. This makes it convenient to memcpy + */ + struct AttenuationTerms + { + float constant; + float linear; + float quadratic; + }; + + /** Light position. */ + Vector3 position_; + + /** Light colour. */ + Colour colour_; + + /** Attenuation terms. */ + AttenuationTerms attenuation_terms_; +}; + +} diff --git a/include/iris/graphics/macos/macos_window.h b/include/iris/graphics/macos/macos_window.h new file mode 100644 index 00000000..9bc0d2e3 --- /dev/null +++ b/include/iris/graphics/macos/macos_window.h @@ -0,0 +1,54 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/window.h" + +namespace iris +{ + +/** + * Implementation of Window for macOS. + */ +class MacosWindow : public Window +{ + public: + /** + * Construct a new MacosWindow. + * + * @param width + * Width of window. + * + * @param height + * Height of window. + */ + MacosWindow(std::uint32_t width, std::uint32_t height); + ~MacosWindow() override = default; + + /** + * Get the natural scale for the screen. This value reflects the scale + * factor needed to convert from the default logical coordinate space into + * the device coordinate space of this screen. + * + * @returns + * Screen scale factor. + */ + std::uint32_t screen_scale() const override; + + /** + * Pump the next user input event. Result will be empty if there are no + * new events. + * + * @returns + * Optional event. + */ + std::optional pump_event() override; +}; + +} diff --git a/include/iris/graphics/macos/macos_window_manager.h b/include/iris/graphics/macos/macos_window_manager.h new file mode 100644 index 00000000..7a65fb22 --- /dev/null +++ b/include/iris/graphics/macos/macos_window_manager.h @@ -0,0 +1,50 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "graphics/window.h" +#include "graphics/window_manager.h" + +namespace iris +{ + +/** + * Implementation of WindowManager for macos. + */ +class MacosWindowManager : public WindowManager +{ + public: + ~MacosWindowManager() override = default; + + /** + * Create a new Window. + * + * @param width + * Width of window. + * + * @param height + * Height of window. + */ + Window *create_window(std::uint32_t width, std::uint32_t height) override; + + /** + * Get the currently active window. + * + * @returns + * Pointer to current window, nullptr if one does not exist. + */ + Window *current_window() const override; + + private: + /** Current window .*/ + std::unique_ptr current_window_; +}; + +} diff --git a/include/iris/graphics/macos/metal_app_delegate.h b/include/iris/graphics/macos/metal_app_delegate.h new file mode 100644 index 00000000..a4fee0a2 --- /dev/null +++ b/include/iris/graphics/macos/metal_app_delegate.h @@ -0,0 +1,31 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#import + +/** + * Delegate for our app which will handle window creation + */ +@interface MetalAppDelegate : NSObject +{ +} + +/** + * Initialise a new MetalAppDelegate with an OpenGl window of the specified + * dimensions. + * + * @param rect + * Dimensions of new window. + */ +- (id)initWithRect:(NSRect)rect; + +/** Width of OpenGl window. */ +@property(assign) CGFloat width; + +/** Height of OpenGl window. */ +@property(assign) CGFloat height; + +@end diff --git a/include/iris/graphics/macos/metal_view.h b/include/iris/graphics/macos/metal_view.h new file mode 100644 index 00000000..c87e4576 --- /dev/null +++ b/include/iris/graphics/macos/metal_view.h @@ -0,0 +1,11 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#import + +@interface MetalView : MTKView + +@end diff --git a/include/iris/graphics/macos/opengl_app_delegate.h b/include/iris/graphics/macos/opengl_app_delegate.h new file mode 100644 index 00000000..73236cfa --- /dev/null +++ b/include/iris/graphics/macos/opengl_app_delegate.h @@ -0,0 +1,31 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#import + +/** + * Delegate for our app which will handle window creation + */ +@interface OpenGLAppDelegate : NSObject +{ +} + +/** + * Initialise a new OpenGLAppDelegate with an OpenGl window of the specified + * dimensions. + * + * @param rect + * Dimensions of new window. + */ +- (id)initWithRect:(NSRect)rect; + +/** Width of OpenGl window. */ +@property(assign) CGFloat width; + +/** Height of OpenGl window. */ +@property(assign) CGFloat height; + +@end diff --git a/include/iris/graphics/macos/opengl_view.h b/include/iris/graphics/macos/opengl_view.h new file mode 100644 index 00000000..87705ccf --- /dev/null +++ b/include/iris/graphics/macos/opengl_view.h @@ -0,0 +1,11 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#import + +@interface OpenGLView : NSOpenGLView + +@end diff --git a/include/iris/graphics/material.h b/include/iris/graphics/material.h new file mode 100644 index 00000000..d889f6e5 --- /dev/null +++ b/include/iris/graphics/material.h @@ -0,0 +1,37 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/texture.h" + +namespace iris +{ + +/** + * Interface for a Material - a class which which encapsulates how to render + * a Mesh. + * + * This interface is deliberately limited, most of the functionality is provided + * by the implementations, which in turn is only used internally by the engine. + */ +class Material +{ + public: + virtual ~Material() = default; + + /** + * Get all the textures used by this Material + * + * @returns + * Collection of Texture objects. + */ + virtual std::vector textures() const = 0; +}; + +} diff --git a/include/iris/graphics/mesh.h b/include/iris/graphics/mesh.h new file mode 100644 index 00000000..71ecdb2a --- /dev/null +++ b/include/iris/graphics/mesh.h @@ -0,0 +1,43 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "graphics/vertex_data.h" + +namespace iris +{ + +/** + * Interface for a Mesh - a class which encapsulates all the vertex data needed + * to render a mesh. + */ +class Mesh +{ + public: + virtual ~Mesh(); + + /** + * Update the vertex data, this will also update any GPU data. + * + * @param data + * New vertex data. + */ + virtual void update_vertex_data(const std::vector &data) = 0; + + /** + * Update the index data, this will also update any GPU data. + * + * @param data + * New index data. + */ + virtual void update_index_data(const std::vector &data) = 0; +}; + +} diff --git a/include/iris/graphics/mesh_loader.h b/include/iris/graphics/mesh_loader.h new file mode 100644 index 00000000..f0b25b93 --- /dev/null +++ b/include/iris/graphics/mesh_loader.h @@ -0,0 +1,46 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "graphics/skeleton.h" +#include "graphics/texture.h" +#include "graphics/vertex_data.h" + +namespace iris::mesh_loader +{ + +/** + * Helper struct encapsulating loaded mesh data. + */ +struct LoadedData +{ + /** Mesh vertices. */ + std::vector vertices; + + /** Mesh indices. */ + std::vector indices; + + /** Mesh skeleton. */ + Skeleton skeleton; +}; + +/** + * Load a mesh from file and return its data.c + * + * @param mesh_name + * Name of of mesh to load, will be passed to ResourceLoader. + * + * @returns + * Data loaded fro file. + */ +LoadedData load(const std::string &mesh_name); + +} diff --git a/include/iris/graphics/mesh_manager.h b/include/iris/graphics/mesh_manager.h new file mode 100644 index 00000000..4409bff1 --- /dev/null +++ b/include/iris/graphics/mesh_manager.h @@ -0,0 +1,150 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +#include "core/colour.h" +#include "core/vector3.h" +#include "graphics/mesh.h" +#include "graphics/skeleton.h" +#include "graphics/texture.h" +#include "graphics/vertex_data.h" + +namespace iris +{ + +/** + * Abstract class for creating and managing Mesh objects. This class handles + * caching and lifetime management of all created objects. Implementers just + * need to provide a graphics API specific method for creating Mesh objects. + */ +class MeshManager +{ + public: + MeshManager(); + virtual ~MeshManager() = default; + + /** + * Create a Sprite mesh. + * + * @param colour + * Colour of sprite. + * + * @returns + * Mesh for sprite. + */ + Mesh *sprite(const Colour &colour); + + /** + * Create a cube mesh. + * + * @param colour + * Colour of cube. + * + * @returns + * Mesh for cube. + */ + Mesh *cube(const Colour &colour); + + /** + * Create a plane mesh. + * + * @param colour + * Colour of plane. + * + * @param divisions + * Number of divisions (both horizontal and vertical). + * + * @returns + * Mesh for cube. + */ + Mesh *plane(const Colour &colour, std::uint32_t divisions); + + /** + * Create a Quad mesh. + * + * @param colour + * Colour of quad. + * + * @param lower_left + * World coords of lower left of quad. + * + * @param lower_right + * World coords of lower right of quad. + * + * @param upper_left + * World coords of upper left of quad. + * + * @param upper_right + * World coords of upper right of quad. + * + * @returns + * Mesh for sprite. + */ + Mesh *quad( + const Colour &colour, + const Vector3 &lower_left, + const Vector3 &lower_right, + const Vector3 &upper_left, + const Vector3 &upper_right); + + /** + * Load a mesh from file. + * + * @param mesh_file + * File to load. + * + * @returns + * Mesh loaded from file. + */ + Mesh *load_mesh(const std::string &mesh_file); + + /** + * Load a skeleton from a file. + * + * Note that unlike load_mesh this returns a new copy each time, this is so + * each Skeleton can be mutated independently. + * + * @param mesh_file + * File to load. + * + * @returns + * Skeleton loaded from file. + * + */ + Skeleton load_skeleton(const std::string &mesh_file); + + protected: + /** + * Create a Mesh object from the provided vertex and index data. + * + * @param vertices + * Collection of vertices for the Mesh. + * + * @param indices + * Collection of indices fro the Mesh. + * + * @returns + * Loaded Mesh. + */ + virtual std::unique_ptr create_mesh( + const std::vector &vertices, + const std::vector &indices) const = 0; + + private: + /** Cache of created Mesh objects. */ + std::unordered_map> loaded_meshes_; + + /** Collection of created Skeleton objects. */ + std::unordered_map loaded_skeletons_; +}; + +} diff --git a/include/iris/graphics/metal/compiler_strings.h b/include/iris/graphics/metal/compiler_strings.h new file mode 100644 index 00000000..8ef50e14 --- /dev/null +++ b/include/iris/graphics/metal/compiler_strings.h @@ -0,0 +1,199 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +static constexpr auto preamble = R"( +#include +#include +#include +using namespace metal; +)"; + +static constexpr auto vertex_in = R"( +typedef struct +{ + float4 position; + float4 normal; + float4 color; + float4 tex; + float4 tangent; + float4 bitangent; + int4 bone_ids; + float4 bone_weights; +} VertexIn; +)"; + +static constexpr auto vertex_out = R"( +typedef struct +{ + float4 position [[position]]; + float4 frag_position; + float3 tangent_view_pos; + float3 tangent_frag_pos; + float3 tangent_light_pos; + float4 frag_pos_light_space; + float4 normal; + float4 color; + float4 tex; +} VertexOut; +)"; + +static constexpr auto default_uniform = R"( +typedef struct +{ + float4x4 projection; + float4x4 view; + float4x4 model; + float4x4 normal_matrix; + float4x4 bones[100]; + float4 camera; + float4 light_colour; + float4 light_position; + float light_attenuation[3]; + float time; +} DefaultUniform; +)"; + +static constexpr auto directional_light_uniform = R"( +typedef struct +{ + float4x4 proj; + float4x4 view; +} DirectionalLightUniform; +)"; + +static constexpr auto point_light_uniform = R"( +typedef struct +{ + float4 position; +} PointLightUniform; +)"; + +static constexpr auto vertex_begin = R"( +float4x4 bone_transform = calculate_bone_transform(uniform, vid, vertices); +float2 uv = vertices[vid].tex.xy; + +VertexOut out; +out.frag_position = uniform->model * bone_transform * vertices[vid].position; +out.position = uniform->projection * uniform->view * out.frag_position; +out.normal = uniform->normal_matrix * bone_transform * vertices[vid].normal; +out.color = vertices[vid].color; +out.tex = vertices[vid].tex; + +const float3x3 tbn = calculate_tbn(uniform, bone_transform, vid, vertices); + +out.tangent_light_pos = tbn * uniform->light_position.xyz; +out.tangent_view_pos = tbn * uniform->camera.xyz; +out.tangent_frag_pos = tbn * out.frag_position.xyz; +)"; + +static constexpr auto blur_function = R"( +float4 blur(texture2d texture, float2 tex_coords) +{ + constexpr sampler s(coord::normalized, address::repeat, filter::linear); + + const float offset = 1.0 / 500.0; + float2 offsets[9] = { + float2(-offset, offset), // top-left + float2( 0.0f, offset), // top-center + float2( offset, offset), // top-right + float2(-offset, 0.0f), // center-left + float2( 0.0f, 0.0f), // center-center + float2( offset, 0.0f), // center-right + float2(-offset, -offset), // bottom-left + float2( 0.0f, -offset), // bottom-center + float2( offset, -offset) // bottom-right + }; + + float k[9] = { + 1.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0, + 2.0 / 16.0, 4.0 / 16.0, 2.0 / 16.0, + 1.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0 + }; + + float3 sampleTex[9]; + for(int i = 0; i < 9; i++) + { + sampleTex[i] = float3(texture.sample(s, tex_coords + offsets[i])); + } + float3 col = float3(0.0); + for(int i = 0; i < 9; i++) + { + col += sampleTex[i] * k[i]; + } + return float4(col, 1.0); +})"; + +static constexpr auto composite_function = R"( +float4 composite(float4 colour1, float4 colour2, float4 depth1, float4 depth2, float2 tex_coord) +{ + float4 colour = colour2; + + if(depth1.r < depth2.r) + { + colour = colour1; + } + + return colour; +})"; + +static constexpr auto invert_function = R"( +float4 invert(float4 colour) +{ + return float4(float3(1.0 - colour), 1.0); +})"; + +static constexpr auto bone_transform_function = R"( +float4x4 calculate_bone_transform(constant DefaultUniform *uniform, uint vid, device VertexIn *vertices) +{ + float4x4 bone_transform = uniform->bones[vertices[vid].bone_ids.x] * vertices[vid].bone_weights.x; + bone_transform += uniform->bones[vertices[vid].bone_ids.y] * vertices[vid].bone_weights.y; + bone_transform += uniform->bones[vertices[vid].bone_ids.z] * vertices[vid].bone_weights.z; + bone_transform += uniform->bones[vertices[vid].bone_ids.w] * vertices[vid].bone_weights.w; + + return transpose(bone_transform); + +})"; + +static constexpr auto tbn_function = R"( +float3x3 calculate_tbn(constant DefaultUniform *uniform, float4x4 bone_transform, uint vid, device VertexIn *vertices) +{ + float3 T = normalize(float3(uniform->normal_matrix * bone_transform * vertices[vid].tangent)); + float3 B = normalize(float3(uniform->normal_matrix * bone_transform * vertices[vid].bitangent)); + float3 N = normalize(float3(uniform->normal_matrix * bone_transform * vertices[vid].normal)); + return transpose(float3x3(T, B, N)); +})"; + +static constexpr auto shadow_function = R"( +float calculate_shadow(float3 n, float4 frag_pos_light_space, float3 light_dir, texture2d texture, sampler smp) +{ + float shadow = 0.0; + + float3 proj_coord = frag_pos_light_space.xyz / frag_pos_light_space.w; + + float2 proj_uv = float2(proj_coord.x, -proj_coord.y); + proj_uv = proj_uv * 0.5 + 0.5; + + float closest_depth = texture.sample(smp, proj_uv).r; + float current_depth = proj_coord.z; + float bias = 0.001; + + shadow = current_depth - bias > closest_depth ? 1.0 : 0.0; + if(proj_coord.z > 1.0) + { + shadow = 0.0; + } + + return shadow; +})"; + +} diff --git a/include/iris/graphics/metal/metal_buffer.h b/include/iris/graphics/metal/metal_buffer.h new file mode 100644 index 00000000..875f0029 --- /dev/null +++ b/include/iris/graphics/metal/metal_buffer.h @@ -0,0 +1,89 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#import + +#include "graphics/vertex_data.h" + +namespace iris +{ + +/** + * This class encapsulates a metal buffer. A buffer can be created with either + * vertex or index data. + */ +class MetalBuffer +{ + public: + /** + * Construct a new MetalBuffer with vertex data. + * + * @param vertex_data + * Vertex data to copy to buffer. + */ + MetalBuffer(const std::vector &vertex_data); + + /** + * Construct a new MetalBuffer with index data. + * + * @param vertex_data + * Index data to copy to buffer. + */ + MetalBuffer(const std::vector &index_data); + + MetalBuffer(const MetalBuffer &) = delete; + MetalBuffer &operator=(const MetalBuffer &) = delete; + + /** + * Get the metal handle to the buffer. + * + * @returns + * Metal handle. + */ + id handle() const; + + /** + * Get the number of elements stored in the buffer. + * + * @returns + * Number of elements in buffer. + */ + std::size_t element_count() const; + + /** + * Write vertex data to the buffer. + * + * @param vertex_data + * New vertex data. + */ + void write(const std::vector &vertex_data); + + /** + * Write index data to the buffer. + * + * @param index_data + * New index data. + */ + void write(const std::vector &index_data); + + private: + /** Metal handle for buffer. */ + id handle_; + + /** Number of elements in buffer. */ + std::size_t element_count_; + + /** Maximum number of elements that can be stored in buffer. */ + std::size_t capacity_; +}; + +} diff --git a/include/iris/graphics/metal/metal_constant_buffer.h b/include/iris/graphics/metal/metal_constant_buffer.h new file mode 100644 index 00000000..6096b624 --- /dev/null +++ b/include/iris/graphics/metal/metal_constant_buffer.h @@ -0,0 +1,91 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#import + +#include "core/exception.h" + +namespace iris +{ + +/** + * This class encapsulates a constant shader buffer. This is data that is set + * once then made available to all vertices/fragments. It is analogous to an + * OpenGL uniform. + */ +class MetalConstantBuffer +{ + public: + /** + * Construct a new MetalConstantBuffer. + * + * @param capacity + * Size (in bytes) of buffer. + */ + MetalConstantBuffer(std::size_t capacity); + + MetalConstantBuffer(const MetalConstantBuffer &) = delete; + MetalConstantBuffer &operator=(const MetalConstantBuffer &) = delete; + + /** + * Write an object into the buffer at an offset. + * + * @param object + * Object to write. + * + * @param offset + * Offset into buffer to write object. + */ + template + void write(const T &object, std::size_t offset) + { + write(std::addressof(object), sizeof(T), offset); + } + + /** + * Write an object into the buffer at an offset. + * + * @param object + * Object to write. + * + * @param size + * Size (in bytes) of object to write. + * + * @param offset + * Offset into buffer to write object. + */ + template + void write(const T *object, std::size_t size, std::size_t offset) + { + if (offset + size > capacity_) + { + throw Exception("write would overflow"); + } + + std::memcpy(static_cast(buffer_.contents) + offset, object, size); + } + + /** + * Get metal handle to buffer. + * + * @returns + * Metal handle. + */ + id handle() const; + + private: + /** Metal handle to buffer. */ + id buffer_; + + /** Capacity (in bytes) of buffer. */ + std::size_t capacity_; +}; + +} diff --git a/include/iris/graphics/metal/metal_default_constant_buffer_types.h b/include/iris/graphics/metal/metal_default_constant_buffer_types.h new file mode 100644 index 00000000..f32844d4 --- /dev/null +++ b/include/iris/graphics/metal/metal_default_constant_buffer_types.h @@ -0,0 +1,37 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "core/matrix4.h" + +namespace iris +{ + +// this file Fontaine's various structs which map to MSL structs. + +struct DefaultConstantBuffer +{ + Matrix4 projection; + Matrix4 view; + Matrix4 model; + Matrix4 normal_matrix; + Matrix4 bones[100]; + float camera[4]; + float light_position[4]; + float light_world_space[4]; + float light_attenuation[3]; + float time; + char padding[48]; +}; + +struct DirectionalLightConstantBuffer +{ + Matrix4 proj; + Matrix4 view; +}; + +} diff --git a/include/iris/graphics/metal/metal_material.h b/include/iris/graphics/metal/metal_material.h new file mode 100644 index 00000000..72b0ac14 --- /dev/null +++ b/include/iris/graphics/metal/metal_material.h @@ -0,0 +1,67 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#import + +#include "graphics/lights/light_type.h" +#include "graphics/material.h" +#include "graphics/render_graph/render_graph.h" +#include "graphics/texture.h" + +namespace iris +{ + +/** + * Implementation of Material for metal. + */ +class MetalMaterial : public Material +{ + public: + /** + * Construct a new MetalMaterial. + * + * @param render_graph + * RenderGraph that describes the material. + * + * @param descriptors + * Metal vertex descriptor describing how to organise vertex data. + * + * @param light_type + * Type of light for this material. + */ + MetalMaterial(const RenderGraph *render_graph, MTLVertexDescriptor *descriptors, LightType light_type); + + ~MetalMaterial() override = default; + + /** + * Get all textures used by this material. + * + * @returns + * Collection of Texture objects used by this material. + */ + std::vector textures() const override; + + /** + * Get the metal pipeline state for this material. + * + * @returns + * Pipeline state. + */ + id pipeline_state() const; + + private: + /** Pipeline state object. */ + id pipeline_state_; + + /** Collection of Texture objects used by material. */ + std::vector textures_; +}; + +} diff --git a/include/iris/graphics/metal/metal_mesh.h b/include/iris/graphics/metal/metal_mesh.h new file mode 100644 index 00000000..14713a7c --- /dev/null +++ b/include/iris/graphics/metal/metal_mesh.h @@ -0,0 +1,97 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#import + +#include "graphics/mesh.h" +#include "graphics/metal/metal_buffer.h" +#include "graphics/vertex_data.h" + +namespace iris +{ + +/** + * Implementation of Mesh for metal. + */ +class MetalMesh : public Mesh +{ + public: + /** + * Construct a new MetalMesh. + * + * @param vertices + * Vertices for the mesh. + * + * @param indices + * Indices for the mesh. + * + * @param attributes + * Attributes of the vertices. + */ + MetalMesh( + const std::vector &vertices, + const std::vector &indices, + const VertexAttributes &attributes); + + ~MetalMesh() override = default; + + /** + * Update the vertex data, this will also update any GPU data. + * + * @param data + * New vertex data. + */ + void update_vertex_data(const std::vector &data) override; + + /** + * Update the index data, this will also update any GPU data. + * + * @param data + * New index data. + */ + void update_index_data(const std::vector &data) override; + + /** + * Get vertex buffer. + * + * @returns + * Const reference to vertex buffer object. + */ + const MetalBuffer &vertex_buffer() const; + + /** + * Get index buffer. + * + * @returns + * Const reference to index buffer object. + */ + const MetalBuffer &index_buffer() const; + + /** + * Get Metal object which describes vertex layout. + * + * @param + * Metal object describing vertex. + */ + MTLVertexDescriptor *descriptors() const; + + private: + /** Buffer for vertex data. */ + MetalBuffer vertex_buffer_; + + /** Buffer for index data. */ + MetalBuffer index_buffer_; + + /** Metal object describing vertex layout. */ + MTLVertexDescriptor *descriptors_; +}; + +} diff --git a/include/iris/graphics/metal/metal_mesh_manager.h b/include/iris/graphics/metal/metal_mesh_manager.h new file mode 100644 index 00000000..6617f571 --- /dev/null +++ b/include/iris/graphics/metal/metal_mesh_manager.h @@ -0,0 +1,46 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "graphics/mesh.h" +#include "graphics/mesh_manager.h" +#include "graphics/vertex_data.h" + +namespace iris +{ + +/** + * Implementation of MeshManager for metal. + */ +class MetalMeshManager : public MeshManager +{ + public: + ~MetalMeshManager() override = default; + + protected: + /** + * Create a Mesh object from the provided vertex and index data. + * + * @param vertices + * Collection of vertices for the Mesh. + * + * @param indices + * Collection of indices fro the Mesh. + * + * @returns + * Loaded Mesh. + */ + std::unique_ptr create_mesh( + const std::vector &vertices, + const std::vector &indices) const override; +}; + +} diff --git a/include/iris/graphics/metal/metal_render_target.h b/include/iris/graphics/metal/metal_render_target.h new file mode 100644 index 00000000..8298cbd7 --- /dev/null +++ b/include/iris/graphics/metal/metal_render_target.h @@ -0,0 +1,37 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/metal/metal_texture.h" +#include "graphics/render_target.h" + +namespace iris +{ + +/** + * Implementation of RenderTarget for metal. + */ +class MetalRenderTarget : public RenderTarget +{ + public: + /** + * Construct a new MetalRenderTarget. + * + * @param colour_texture + * Texture to render colour data to. + * + * @param depth_texture + * Texture to render depth data to. + */ + MetalRenderTarget(std::unique_ptr colour_texture, std::unique_ptr depth_texture); + + ~MetalRenderTarget() override = default; +}; + +} diff --git a/include/iris/graphics/metal/metal_renderer.h b/include/iris/graphics/metal/metal_renderer.h new file mode 100644 index 00000000..0463c4db --- /dev/null +++ b/include/iris/graphics/metal/metal_renderer.h @@ -0,0 +1,159 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include + +#import +#import +#import + +#include "core/root.h" +#include "graphics/metal/metal_constant_buffer.h" +#include "graphics/metal/metal_material.h" +#include "graphics/metal/metal_render_target.h" +#include "graphics/render_command.h" +#include "graphics/render_graph/render_graph.h" +#include "graphics/renderer.h" +#include "graphics/window_manager.h" + +namespace iris +{ + +/** + * Implementation of Renderer for metal. + * + * This Renderer uses triple buffering to allow for greatest rendering + * throughput. A frame if defined as all rendering passes that occur when + * render() is called. This class uses a circular buffer of three frames. When + * all the CPU processing of a frame is complete it is submitted to the GPU. At + * this point the CPU is free to proceed to the next frame whilst the GPU works + * asynchronously. + */ +class MetalRenderer : public Renderer +{ + public: + /** + * Construct a new MetalRenderer. + * + * @param width + * Width of window being rendered to. + * + * @param height + * Height of window being rendered to. + */ + MetalRenderer(std::uint32_t width, std::uint32_t height); + + /** + * Destructor, will block until all inflight frames have finished rendering. + */ + ~MetalRenderer() override; + + /** + * Set the render passes. These will be executed when render() is called. + * + * @param render_passes + * Collection of RenderPass objects to render. + */ + void set_render_passes(const std::vector &render_passes) override; + + /** + * Create a RenderTarget with custom dimensions. + * + * @param width + * Width of render target. + * + * @param height + * Height of render target. + * + * @returns + * RenderTarget. + */ + RenderTarget *create_render_target(std::uint32_t width, std::uint32_t height) override; + + protected: + // handlers for the supported RenderCommandTypes + + void pre_render() override; + void execute_draw(RenderCommand &command) override; + void execute_pass_end(RenderCommand &command) override; + void execute_present(RenderCommand &command) override; + void post_render() override; + + private: + // helper aliases to try and simplify the verbose types + using LightMaterialMap = std::unordered_map>; + + /** + * Internal struct encapsulating data needed for a frame. + */ + struct Frame + { + /** + * Lock to ensure GPU has finished executing frame before we try and + * write to it again. + * */ + std::mutex lock; + + /** + * Map of render commands to constant buffers - this ensures each draw + * command gets its own buffer. + */ + std::unordered_map constant_data_buffers; + }; + + /** Width of window to render to. */ + std::uint32_t width_; + + /** Height of window to render to. */ + std::uint32_t height_; + + /** Current command queue. */ + id command_queue_; + + /** Default descriptor for all render passes. */ + MTLRenderPassDescriptor *descriptor_; + + /** Current metal drawable. */ + id drawable_; + + /** Current command buffer. */ + id command_buffer_; + + /** Default state for depth buffers. */ + id depth_stencil_state_; + + /** Current render encoder. */ + id render_encoder_; + + /** Current frame number. */ + std::size_t current_frame_; + + /** Triple buffered frames. */ + std::array frames_; + + /** Map of targets to render encoders. */ + std::unordered_map> render_encoders_; + + /** Collection of created RenderTarget objects. */ + std::vector> render_targets_; + + /** This collection stores materials per light type per render graph. */ + std::unordered_map materials_; + + /** The depth buffer for the default frame. */ + std::unique_ptr default_depth_buffer_; + + /** Default sampler for shadow maps. */ + id shadow_sampler_; +}; + +} diff --git a/include/iris/graphics/metal/metal_texture.h b/include/iris/graphics/metal/metal_texture.h new file mode 100644 index 00000000..595e8ac2 --- /dev/null +++ b/include/iris/graphics/metal/metal_texture.h @@ -0,0 +1,58 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#import + +#include "core/data_buffer.h" +#include "graphics/texture.h" +#include "graphics/texture_usage.h" + +namespace iris +{ + +/** + * Implementation of Texture for metal. + */ +class MetalTexture : public Texture +{ + public: + /** + * Construct a new MetalTexture. + * + * @param data + * Image data. This should be width * hight of pixel_format tuples. + * + * @param width + * Width of image. + * + * @param height + * Height of data. + * + * @param usage + * Texture usage. + */ + MetalTexture(const DataBuffer &data, std::uint32_t width, std::uint32_t height, TextureUsage usage); + + ~MetalTexture() override = default; + + /** + * Get metal handle to texture. + * + * @returns + * Metal texture handle. + */ + id handle() const; + + private: + /** Metal texture handle. */ + id texture_; +}; + +} diff --git a/include/iris/graphics/metal/metal_texture_manager.h b/include/iris/graphics/metal/metal_texture_manager.h new file mode 100644 index 00000000..01c80ef1 --- /dev/null +++ b/include/iris/graphics/metal/metal_texture_manager.h @@ -0,0 +1,51 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "core/data_buffer.h" +#include "graphics/texture.h" +#include "graphics/texture_manager.h" +#include "graphics/texture_usage.h" + +namespace iris +{ + +/** + * Implementation of TextureManager for metal. + */ +class MetalTextureManager : public TextureManager +{ + public: + ~MetalTextureManager() override = default; + + protected: + /** + * Create a Texture object with the provided data. + * + * @param data + * Raw data of image, in pixel_format. + * + * @param width + * Width of image. + * + * @param height + * Height of image. + * + * @param usage + * Usage of the texture. + */ + std::unique_ptr do_create( + const DataBuffer &data, + std::uint32_t width, + std::uint32_t height, + TextureUsage usage) override; +}; + +} diff --git a/include/iris/graphics/metal/msl_shader_compiler.h b/include/iris/graphics/metal/msl_shader_compiler.h new file mode 100644 index 00000000..0ad1ec81 --- /dev/null +++ b/include/iris/graphics/metal/msl_shader_compiler.h @@ -0,0 +1,133 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +#include "core/colour.h" +#include "core/vector3.h" +#include "graphics/lights/light_type.h" +#include "graphics/render_graph/render_graph.h" +#include "graphics/render_graph/shader_compiler.h" +#include "graphics/texture.h" +#include "graphics/vertex_attributes.h" + +namespace iris +{ + +class RenderNode; +class PostProcessingNode; +class ColourNode; +class TextureNode; +class InvertNode; +class BlurNode; +class CompositeNode; +class VertexPositionNode; +class ArithmeticNode; +class ConditionalNode; +class ComponentNode; +class CombineNode; +class SinNode; +template +class ValueNode; + +/** + * Implementation of ShaderCompiler for MLSL. + */ +class MSLShaderCompiler : public ShaderCompiler +{ + public: + /** + * Construct a new MLSLSHaderCompiler. + * + * @param render_graph + * RenderGraph to compile into MLSL. + * + * @param light_type + * The type of light to render with. + */ + MSLShaderCompiler(const RenderGraph *render_graph, LightType light_type); + + ~MSLShaderCompiler() override = default; + + // visitor methods + void visit(const RenderNode &node) override; + void visit(const PostProcessingNode &node) override; + void visit(const ColourNode &node) override; + void visit(const TextureNode &node) override; + void visit(const InvertNode &node) override; + void visit(const BlurNode &node) override; + void visit(const CompositeNode &node) override; + void visit(const VertexPositionNode &node) override; + void visit(const ValueNode &node) override; + void visit(const ValueNode &node) override; + void visit(const ValueNode &node) override; + void visit(const ArithmeticNode &node) override; + void visit(const ConditionalNode &node) override; + void visit(const ComponentNode &node) override; + void visit(const CombineNode &node) override; + void visit(const SinNode &node) override; + + /** + * Get the compiled vertex shader. + * + * Compiled here means from the render graph to a string, not to an API + * specific object on the hardware. + * + * @returns + * Vertex shader. + */ + std::string vertex_shader() const override; + + /** + * Get the compiled fragment shader. + * + * Compiled here means from the render graph to a string, not to an API + * specific object on the hardware. + * + * @returns + * Fragment shader. + */ + std::string fragment_shader() const override; + + /** + * Collection of textures needed for the shaders. + * + * @returns + * Collection of Textures. + */ + std::vector textures() const override; + + private: + /** Stream for vertex shader. */ + std::stringstream vertex_stream_; + + /** Stream for fragment shader. */ + std::stringstream fragment_stream_; + + /** Pointer to the current shader stream. */ + std::stringstream *current_stream_; + + /** Collection of vertex functions. */ + std::set vertex_functions_; + + /** Collection of fragment functions. */ + std::set fragment_functions_; + + /** Pointer to current function collection. */ + std::set *current_functions_; + + /** Textures needed for shaders. */ + std::vector textures_; + + /** Type of light to render with. */ + LightType light_type_; +}; +} diff --git a/include/iris/graphics/opengl/compiler_strings.h b/include/iris/graphics/opengl/compiler_strings.h new file mode 100644 index 00000000..166f4431 --- /dev/null +++ b/include/iris/graphics/opengl/compiler_strings.h @@ -0,0 +1,182 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +namespace iris +{ + +static constexpr auto preamble = R"( +#version 330 core +precision mediump float; +)"; + +static constexpr auto layouts = R"( +layout (location = 0) in vec4 position; +layout (location = 1) in vec4 normal; +layout (location = 2) in vec4 colour; +layout (location = 3) in vec4 tex; +layout (location = 4) in vec4 tangent; +layout (location = 5) in vec4 bitangent; +layout (location = 6) in ivec4 bone_ids; +layout (location = 7) in vec4 bone_weights; +)"; + +static constexpr auto uniforms = R"( +uniform mat4 projection; +uniform mat4 view; +uniform mat4 model; +uniform mat4 normal_matrix; +uniform mat4 bones[100]; +uniform vec3 camera_; +uniform vec4 light_colour; +uniform vec4 light_position; +uniform float light_attenuation[3]; +uniform mat4 light_projection; +uniform mat4 light_view; +)"; + +static constexpr auto vertex_out = R"( +out vec4 frag_pos; +out vec2 tex_coord; +out vec4 col; +out vec4 norm; +out vec4 frag_pos_light_space; +out vec3 tangent_light_pos; +out vec3 tangent_view_pos; +out vec3 tangent_frag_pos; +)"; + +static constexpr auto fragment_in = R"( +in vec2 tex_coord; +in vec4 col; +in vec4 norm; +in vec4 frag_pos; +in vec4 frag_pos_light_space; +in vec3 tangent_light_pos; +in vec3 tangent_view_pos; +in vec3 tangent_frag_pos; +)"; + +static constexpr auto fragment_out = R"( +out vec4 outColour; +)"; + +static constexpr auto vertex_begin = R"( + mat4 bone_transform = calculate_bone_transform(bone_ids, bone_weights); + mat3 tbn = calculate_tbn(bone_transform); + + col = colour; + norm = normal_matrix * bone_transform * normal; + tex_coord = vec2(tex.x, tex.y); + frag_pos = model * bone_transform * position; + gl_Position = projection * view * frag_pos; + +)"; + +static constexpr auto blur_function = R"( +vec4 blur(sampler2D tex, vec2 tex_coords) +{ + const float offset = 1.0 / 500.0; + vec2 offsets[9] = vec2[]( + vec2(-offset, offset), // top-left + vec2( 0.0f, offset), // top-center + vec2( offset, offset), // top-right + vec2(-offset, 0.0f), // center-left + vec2( 0.0f, 0.0f), // center-center + vec2( offset, 0.0f), // center-right + vec2(-offset, -offset), // bottom-left + vec2( 0.0f, -offset), // bottom-center + vec2( offset, -offset) // bottom-right + ); + + float kernel[9] = float[]( + 1.0 / 16, 2.0 / 16, 1.0 / 16, + 2.0 / 16, 4.0 / 16, 2.0 / 16, + 1.0 / 16, 2.0 / 16, 1.0 / 16 + ); + + vec3 sampleTex[9]; + for(int i = 0; i < 9; i++) + { + sampleTex[i] = vec3(texture(tex, tex_coords.st + offsets[i])); + } + vec3 col = vec3(0.0); + for(int i = 0; i < 9; i++) + { + col += sampleTex[i] * kernel[i]; + } + return vec4(col, 1.0); +})"; + +static constexpr auto composite_function = R"( +vec4 composite(vec4 colour1, vec4 colour2, vec4 depth1, vec4 depth2, vec2 tex_coord) +{ + vec4 colour = colour2; + + if(depth1.r < depth2.r) + { + colour = colour1; + } + + return colour; +} +)"; + +static constexpr auto invert_function = R"( +vec4 invert(vec4 colour) +{ + return vec4(vec3(1.0 - colour), 1.0); +})"; + +static constexpr auto bone_transform_function = R"( +mat4 calculate_bone_transform(ivec4 bone_ids, vec4 bone_weights) +{ + mat4 bone_transform = bones[bone_ids[0]] * bone_weights[0]; + bone_transform += bones[bone_ids[1]] * bone_weights[1]; + bone_transform += bones[bone_ids[2]] * bone_weights[2]; + bone_transform += bones[bone_ids[3]] * bone_weights[3]; + + return bone_transform; + +})"; + +static constexpr auto tbn_function = R"( +mat3 calculate_tbn(mat4 bone_transform) +{ + vec3 T = normalize(vec3(normal_matrix * bone_transform * tangent)); + vec3 B = normalize(vec3(normal_matrix * bone_transform * bitangent)); + vec3 N = normalize(vec3(normal_matrix * bone_transform * normal)); + + return transpose(mat3(T, B, N)); +})"; + +static constexpr auto shadow_function = R"( +float calculate_shadow(vec3 n, vec4 frag_pos_light_space, vec3 light_dir, sampler2D tex) +{ + float shadow = 0.0; + + vec3 proj_coord = frag_pos_light_space.xyz / frag_pos_light_space.w; + proj_coord = proj_coord * 0.5 + 0.5; + + float closest_depth = texture(tex, proj_coord.xy).r; + float current_depth = proj_coord.z; + + float bias = 0.001; + shadow = (current_depth - bias) > closest_depth ? 1.0 : 0.0; + + if(proj_coord.z > 1.0) + { + shadow = 0.0; + } + + return shadow; +})"; + +} diff --git a/include/iris/graphics/opengl/default_uniforms.h b/include/iris/graphics/opengl/default_uniforms.h new file mode 100644 index 00000000..15497852 --- /dev/null +++ b/include/iris/graphics/opengl/default_uniforms.h @@ -0,0 +1,121 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/opengl/opengl_uniform.h" + +namespace iris +{ + +/** + * Struct encapsulating all the default required uniforms for OpenGL rendering. + */ +struct DefaultUniforms +{ + /** + * Construct a new DefaultUniforms. + * + * @param projection + * Projection uniform. + * + * @param view + * View uniform. + * + * @param model + * Model uniform. + * + * @param normal_matrix + * Normal matrix uniform. + * + * @param light_colour + * Colour of the light. + * + * @param light_position + * Position data of the light. + * + * @param light_attenuation + * Attenuation data of the light. + * + * @param shadow_map + * Shadow map uniform. + * + * @param light_projection + * Light camera projection uniform. + * + * @param light_view + * Light camera view uniform. + * + * @param bones + * Bones uniform. + */ + DefaultUniforms( + OpenGLUniform projection, + OpenGLUniform view, + OpenGLUniform model, + OpenGLUniform normal_matrix, + OpenGLUniform light_colour, + OpenGLUniform light_position, + OpenGLUniform light_attenuation, + OpenGLUniform shadow_map, + OpenGLUniform light_projection, + OpenGLUniform light_view, + OpenGLUniform bones) + : projection(projection) + , view(view) + , model(model) + , normal_matrix(normal_matrix) + , light_colour(light_colour) + , light_position(light_position) + , light_attenuation(light_attenuation) + , shadow_map(shadow_map) + , light_projection(light_projection) + , light_view(light_view) + , bones(bones) + , textures() + { + } + + /** Projection uniform. */ + OpenGLUniform projection; + + /** View uniform. */ + OpenGLUniform view; + + /** Model uniform. */ + OpenGLUniform model; + + /** Normal matrix uniform. */ + OpenGLUniform normal_matrix; + + /** Colour of the light. */ + OpenGLUniform light_colour; + + /** Position data of the light. */ + OpenGLUniform light_position; + + /** Attenuation data of the light. */ + OpenGLUniform light_attenuation; + + /** Shadow map uniform. */ + OpenGLUniform shadow_map; + + /** Light camera projection uniform. */ + OpenGLUniform light_projection; + + /** Light camera view uniform. */ + OpenGLUniform light_view; + + /** Bones uniform. */ + OpenGLUniform bones; + + /** Collection of texture uniforms. */ + std::vector textures; +}; + +} diff --git a/include/iris/graphics/opengl/glsl_shader_compiler.h b/include/iris/graphics/opengl/glsl_shader_compiler.h new file mode 100644 index 00000000..281aa5b6 --- /dev/null +++ b/include/iris/graphics/opengl/glsl_shader_compiler.h @@ -0,0 +1,133 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +#include "core/colour.h" +#include "core/vector3.h" +#include "graphics/lights/light_type.h" +#include "graphics/render_graph/render_graph.h" +#include "graphics/render_graph/shader_compiler.h" +#include "graphics/texture.h" +#include "graphics/vertex_attributes.h" + +namespace iris +{ + +class RenderNode; +class PostProcessingNode; +class ColourNode; +class TextureNode; +class InvertNode; +class BlurNode; +class CompositeNode; +class VertexPositionNode; +class ArithmeticNode; +class ConditionalNode; +class ComponentNode; +class CombineNode; +class SinNode; +template +class ValueNode; + +/** + * Implementation of ShaderCompiler for GLSL. + */ +class GLSLShaderCompiler : public ShaderCompiler +{ + public: + /** + * Construct a new GLSLSHaderCompiler. + * + * @param render_graph + * RenderGraph to cpmpile into GLSL. + * + * @param light_type + * The type of light to render with. + */ + GLSLShaderCompiler(const RenderGraph *render_graph, LightType light_type); + + ~GLSLShaderCompiler() override = default; + + // visitor methods + void visit(const RenderNode &node) override; + void visit(const PostProcessingNode &node) override; + void visit(const ColourNode &node) override; + void visit(const TextureNode &node) override; + void visit(const InvertNode &node) override; + void visit(const BlurNode &node) override; + void visit(const CompositeNode &node) override; + void visit(const VertexPositionNode &node) override; + void visit(const ValueNode &node) override; + void visit(const ValueNode &node) override; + void visit(const ValueNode &node) override; + void visit(const ArithmeticNode &node) override; + void visit(const ConditionalNode &node) override; + void visit(const ComponentNode &node) override; + void visit(const CombineNode &node) override; + void visit(const SinNode &node) override; + + /** + * Get the compiled vertex shader. + * + * Compiled here means from the render graph to a string, not to an API + * specific object on the hardware. + * + * @returns + * Vertex shader. + */ + std::string vertex_shader() const override; + + /** + * Get the compiled fragment shader. + * + * Compiled here means from the render graph to a string, not to an API + * specific object on the hardware. + * + * @returns + * Fragment shader. + */ + std::string fragment_shader() const override; + + /** + * Collection of textures needed for the shaders. + * + * @returns + * Collection of Textures. + */ + std::vector textures() const override; + + private: + /** Stream for vertex shader. */ + std::stringstream vertex_stream_; + + /** Stream for fragment shader. */ + std::stringstream fragment_stream_; + + /** Pointer to the current shader stream. */ + std::stringstream *current_stream_; + + /** Collection of vertex functions. */ + std::set vertex_functions_; + + /** Collection of fragment functions. */ + std::set fragment_functions_; + + /** Pointer to current function collection. */ + std::set *current_functions_; + + /** Textures needed for shaders. */ + std::vector textures_; + + /** Type of light to render with. */ + LightType light_type_; +}; +} diff --git a/include/iris/graphics/opengl/opengl.h b/include/iris/graphics/opengl/opengl.h new file mode 100644 index 00000000..87962828 --- /dev/null +++ b/include/iris/graphics/opengl/opengl.h @@ -0,0 +1,58 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +// platform specific opengl includes + +#if defined(IRIS_PLATFORM_MACOS) +#include +#include +#elif defined(IRIS_PLATFORM_WIN32) +// clang-format off +#define WIN32_LEAN_AND_MEAN +#include +#include "gl/gl.h" + +// in order to avoid duplicating all the opengl function definitions as both +// extern and concrete we can use the EXTERN macro to control its linkage +// by default all functions will be marked as extern (the common case) unless +// this include is prefaced with DONT_MAKE_GL_FUNCTIONS_EXTERN +#if defined(DONT_MAKE_GL_FUNCTIONS_EXTERN) +#define EXTERN +#else +#define EXTERN extern +#endif +#include "graphics/opengl/opengl_windows.h" +#pragma comment(lib, "opengl32.lib") +// clang-format on +#else +#error unsupported platform +#endif + +namespace iris +{ + +/** + * Throws an exception if an OpenGl error has occurred. + * + * @param error_message + * The message to include in the exception. + * + * @returns + * Empty optional if no error occurred, otherwise an error string. + */ +std::optional do_check_opengl_error(std::string_view error_message); + +// create std::function so it can be passed to ensure/expect +static const std::function(std::string_view)> check_opengl_error = do_check_opengl_error; + +} diff --git a/include/iris/graphics/opengl/opengl_buffer.h b/include/iris/graphics/opengl/opengl_buffer.h new file mode 100644 index 00000000..69e0458f --- /dev/null +++ b/include/iris/graphics/opengl/opengl_buffer.h @@ -0,0 +1,90 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "graphics/opengl/opengl.h" +#include "graphics/vertex_data.h" + +namespace iris +{ + +/** + * This class encapsulates an opengl buffer. A buffer can be created with either + * vertex or index data. + */ +class OpenGLBuffer +{ + public: + /** + * Construct a new OpenGLBuffer with vertex data. + * + * @param vertex_data + * Vertex data to copy to buffer. + */ + OpenGLBuffer(const std::vector &vertex_data); + + /** + * Construct a new OpenGLBuffer with index data. + * + * @param vertex_data + * Index data to copy to buffer. + */ + OpenGLBuffer(const std::vector &index_data); + + /** + * Clean up opengl objects. + */ + ~OpenGLBuffer(); + + OpenGLBuffer(const OpenGLBuffer &) = delete; + OpenGLBuffer &operator=(const OpenGLBuffer &) = delete; + + /** + * Get the opengl handle to the buffer. + * + * @returns + * OpenGL handle. + */ + GLuint handle() const; + + /** + * Get the number of elements stored in the buffer. + * + * @returns + * Number of elements in buffer. + */ + std::size_t element_count() const; + + /** + * Write vertex data to the buffer. + * + * @param vertex_data + * New vertex data. + */ + void write(const std::vector &vertex_data); + + /** + * Write index data to the buffer. + * + * @param index_data + * New index data. + */ + void write(const std::vector &index_data); + + private: + /** OpenGL handle for buffer. */ + GLuint handle_; + + /** Number of elements in buffer. */ + std::size_t element_count_; +}; + +} \ No newline at end of file diff --git a/include/iris/graphics/opengl/opengl_material.h b/include/iris/graphics/opengl/opengl_material.h new file mode 100644 index 00000000..b39cbde3 --- /dev/null +++ b/include/iris/graphics/opengl/opengl_material.h @@ -0,0 +1,71 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/lights/light_type.h" +#include "graphics/material.h" +#include "graphics/opengl/opengl.h" +#include "graphics/render_graph/render_graph.h" +#include "graphics/texture.h" + +namespace iris +{ + +/** + * Implementation of Material for OpenGL. + */ +class OpenGLMaterial : public Material +{ + public: + /** + * Construct a new OpenGLMaterial. + * + * @param render_graph + * RenderGraph that describes the material. + * + * @param light_type + * Type of light for this material. + */ + OpenGLMaterial(const RenderGraph *render_graph, LightType light_type); + + /** + * Clean up OpenGL objects. + */ + ~OpenGLMaterial() override; + + /** + * Bind this material for rendering with. + */ + void bind() const; + + /** + * Get the OpenGL handle to this material. + * + * @returns + * OpenGL handle. + */ + GLuint handle() const; + + /** + * Get all textures used by this material. + * + * @returns + * Collection of Texture objects used by this material. + */ + std::vector textures() const override; + + private: + /** OpenGL handle to material. */ + GLuint handle_; + + /** Collection of Texture objects used by material. */ + std::vector textures_; +}; + +} diff --git a/include/iris/graphics/opengl/opengl_mesh.h b/include/iris/graphics/opengl/opengl_mesh.h new file mode 100644 index 00000000..6496433a --- /dev/null +++ b/include/iris/graphics/opengl/opengl_mesh.h @@ -0,0 +1,94 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "graphics/mesh.h" +#include "graphics/opengl/opengl.h" +#include "graphics/opengl/opengl_buffer.h" +#include "graphics/vertex_attributes.h" +#include "graphics/vertex_data.h" + +namespace iris +{ + +/** + * Implementation of Mesh for OpenGL. + */ +class OpenGLMesh : public Mesh +{ + public: + /** + * Construct a new OpenGLMesh. + * + * @param vertices + * Vertices for the mesh. + * + * @param indices + * Indices for the mesh. + * + * @param attributes + * Attributes of the vertices. + */ + OpenGLMesh( + const std::vector &vertices, + const std::vector &indices, + const VertexAttributes &attributes); + + /** + * Clean up OpenGL objects. + */ + ~OpenGLMesh(); + + /** + * Update the vertex data, this will also update any GPU data. + * + * @param data + * New vertex data. + */ + void update_vertex_data(const std::vector &data) override; + + /** + * Update the index data, this will also update any GPU data. + * + * @param data + * New index data. + */ + void update_index_data(const std::vector &data) override; + + /** + * Get number of elements to be rendered. + * + * @returns + * Number of elements to render. + */ + GLsizei element_count() const; + + /** + * Bind this mesh for rendering. + */ + void bind() const; + + /** + * Unbind this mesh for rendering. + */ + void unbind() const; + + private: + /** Buffer for vertex data. */ + OpenGLBuffer vertex_buffer_; + + /** Buffer for index data. */ + OpenGLBuffer index_buffer_; + + /** OpenGL handle to a vertex array object for this mesh. */ + GLuint vao_; +}; + +} diff --git a/include/iris/graphics/opengl/opengl_mesh_manager.h b/include/iris/graphics/opengl/opengl_mesh_manager.h new file mode 100644 index 00000000..4842271d --- /dev/null +++ b/include/iris/graphics/opengl/opengl_mesh_manager.h @@ -0,0 +1,46 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "graphics/mesh.h" +#include "graphics/mesh_manager.h" +#include "graphics/vertex_data.h" + +namespace iris +{ + +/** + * Implementation of MeshManager for OpenGL. + */ +class OpenGLMeshManager : public MeshManager +{ + public: + ~OpenGLMeshManager() override = default; + + protected: + /** + * Create a Mesh object from the provided vertex and index data. + * + * @param vertices + * Collection of vertices for the Mesh. + * + * @param indices + * Collection of indices fro the Mesh. + * + * @returns + * Loaded Mesh. + */ + std::unique_ptr create_mesh( + const std::vector &vertices, + const std::vector &indices) const override; +}; + +} diff --git a/include/iris/graphics/opengl/opengl_render_target.h b/include/iris/graphics/opengl/opengl_render_target.h new file mode 100644 index 00000000..f176d569 --- /dev/null +++ b/include/iris/graphics/opengl/opengl_render_target.h @@ -0,0 +1,61 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/opengl/opengl.h" +#include "graphics/render_target.h" +#include "graphics/texture.h" + +namespace iris +{ + +/** + * Implementation of RenderTarget for OpenGL. + */ +class OpenGLRenderTarget : public RenderTarget +{ + public: + /** + * Construct a new OpenGLRenderTarget. + * + * @param colour_texture + * Texture to render colour data to. + * + * @param depth_texture + * Texture to render depth data to. + */ + OpenGLRenderTarget(std::unique_ptr colour_texture, std::unique_ptr depth_texture); + + /** + * Clean up OpenGL objects. + */ + ~OpenGLRenderTarget() override; + + /** + * Bind render target for use. + * + * @param target + * OpenGL framebuffer target. + */ + void bind(GLenum target) const; + + /** + * Unbind the render target for use. + * + * @param target + * OpenGL framebuffer target. + */ + void unbind(GLenum target) const; + + private: + /** OpenGL handle to framebuffer */ + GLuint handle_; +}; + +} diff --git a/include/iris/graphics/opengl/opengl_renderer.h b/include/iris/graphics/opengl/opengl_renderer.h new file mode 100644 index 00000000..00108ffc --- /dev/null +++ b/include/iris/graphics/opengl/opengl_renderer.h @@ -0,0 +1,94 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +#include "graphics/opengl/default_uniforms.h" +#include "graphics/opengl/opengl_material.h" +#include "graphics/opengl/opengl_render_target.h" +#include "graphics/opengl/opengl_uniform.h" +#include "graphics/render_graph/render_graph.h" +#include "graphics/renderer.h" + +namespace iris +{ + +/** + * Implementation of Renderer for OpenGL. + */ +class OpenGLRenderer : public Renderer +{ + public: + /** + * Construct a new OpenGLRenderer. + * + * @param width + * Width of window being rendered to. + * + * @param height + * Height of window being rendered to. + */ + OpenGLRenderer(std::uint32_t width, std::uint32_t height); + ~OpenGLRenderer() override = default; + + /** + * Set the render passes. These will be executed when render() is called. + * + * @param render_passes + * Collection of RenderPass objects to render. + */ + void set_render_passes(const std::vector &render_passes) override; + + /** + * Create a RenderTarget with custom dimensions. + * + * @param width + * Width of render target. + * + * @param height + * Height of render target. + * + * @returns + * RenderTarget. + */ + RenderTarget *create_render_target(std::uint32_t width, std::uint32_t height) override; + + protected: + // handlers for the supported RenderCommandTypes + + void execute_pass_start(RenderCommand &command) override; + + void execute_draw(RenderCommand &command) override; + + void execute_present(RenderCommand &command) override; + + private: + // helper aliases to try and simplify the verbose types + using LightMaterialMap = std::unordered_map>; + using EntityUniformMap = std::unordered_map>; + + /** Collection of created RenderTarget objects. */ + std::vector> render_targets_; + + /** This collection stores materials per light type per render graph. */ + std::unordered_map materials_; + + /** This collecion stores DefaultUniforms per entitiy per material. */ + std::unordered_map uniforms_; + + /** Width of window being rendered to. */ + std::uint32_t width_; + + /** Height of window being rendered to. */ + std::uint32_t height_; +}; + +} diff --git a/include/iris/graphics/opengl/opengl_shader.h b/include/iris/graphics/opengl/opengl_shader.h new file mode 100644 index 00000000..6136709c --- /dev/null +++ b/include/iris/graphics/opengl/opengl_shader.h @@ -0,0 +1,71 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/opengl/opengl.h" +#include "graphics/shader_type.h" + +namespace iris +{ + +/** + * Class encapsulating an opengl shader. + */ +class OpenGLShader +{ + public: + /** + * Construct a new shader. + * + * @param source + * Source of the opengl shader. + * + * @param type + * The type of shader. + */ + OpenGLShader(const std::string &source, ShaderType type); + + /** + * Destructor, performs opengl cleanup. + */ + ~OpenGLShader(); + + /** + * Move constructor, steals the state from the moved-in object. + * + * @param other + * Object to take state from. Do not use after this call. + */ + OpenGLShader(OpenGLShader &&other); + + /** + * Move operator, steals the state from the moved-in object. + * + * @param other + * Object to take state from. Do not use after this call. + */ + OpenGLShader &operator=(OpenGLShader &&); + + /** Disabled */ + OpenGLShader(const OpenGLShader &) = delete; + OpenGLShader &operator=(const OpenGLShader &) = delete; + + /** + * Get the native opengl handle. + * + * @returns native opengl handle. + */ + GLuint native_handle() const; + + private: + /** Opengl shader object. */ + GLuint shader_; +}; + +} diff --git a/include/iris/graphics/opengl/opengl_texture.h b/include/iris/graphics/opengl/opengl_texture.h new file mode 100644 index 00000000..270eb15b --- /dev/null +++ b/include/iris/graphics/opengl/opengl_texture.h @@ -0,0 +1,75 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/opengl/opengl.h" + +#include "core/data_buffer.h" +#include "graphics/texture.h" +#include "graphics/texture_usage.h" + +namespace iris +{ + +/** + * Implementation of Texture for OpenGL. + */ +class OpenGLTexture : public Texture +{ + public: + /** + * Construct a new OpenGLTexture. + * + * @param data + * Image data. This should be width * hight of pixel_format tuples. + * + * @param width + * Width of image. + * + * @param height + * Height of data. + * + * @param usage + * Texture usage. + * + * @param id + * OpenGL texture unit. + */ + OpenGLTexture(const DataBuffer &data, std::uint32_t width, std::uint32_t height, TextureUsage usage, GLuint id); + + /** + * Clean up OpenGL objects. + */ + ~OpenGLTexture() override; + + /** + * Get OpenGL handle to texture. + * + * @returns + * OpenGL texture handle. + */ + GLuint handle() const; + + /** + * Get OpenGL texture unit. + * + * @returns + * OpenGL texture unit. + */ + GLuint id() const; + + private: + /** OpenGL texture handle. */ + GLuint handle_; + + /** OpenGL texture unit. */ + GLuint id_; +}; + +} diff --git a/include/iris/graphics/opengl/opengl_texture_manager.h b/include/iris/graphics/opengl/opengl_texture_manager.h new file mode 100644 index 00000000..22f9d6f5 --- /dev/null +++ b/include/iris/graphics/opengl/opengl_texture_manager.h @@ -0,0 +1,85 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "core/data_buffer.h" +#include "graphics/opengl/opengl.h" +#include "graphics/texture_manager.h" +#include "graphics/texture_usage.h" + +namespace iris +{ + +/** + * Implementation of TextureManager for OpenGL. + */ +class OpenGLTextureManager : public TextureManager +{ + public: + /** + * Construct a new OpenGLTextureManager. + */ + OpenGLTextureManager(); + + ~OpenGLTextureManager() override = default; + + /** + * Get the next texture unit id from a pool of available ids. + * + * @returns + * Next available texture id. + */ + GLuint next_id(); + + /** + * Return an id to the pool. + * + * @param id + * Id to return to pool. + */ + void return_id(GLuint id); + + protected: + /** + * Create a Texture object with the provided data. + * + * @param data + * Raw data of image, in pixel_format. + * + * @param width + * Width of image. + * + * @param height + * Height of image. + * + * @param usage + * Usage of the texture. + */ + std::unique_ptr do_create( + const DataBuffer &data, + std::uint32_t width, + std::uint32_t height, + TextureUsage usage) override; + + /** + * Unload a texture. + * + * @param texture + * Texture about to be unloaded. + */ + void destroy(Texture *texture) override; + + private: + /** Stack of available ids. */ + std::stack id_pool_; +}; + +} diff --git a/include/iris/graphics/opengl/opengl_uniform.h b/include/iris/graphics/opengl/opengl_uniform.h new file mode 100644 index 00000000..db53b720 --- /dev/null +++ b/include/iris/graphics/opengl/opengl_uniform.h @@ -0,0 +1,91 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +#include "core/matrix4.h" +#include "graphics/opengl/opengl.h" + +namespace iris +{ + +/** + * This class encapsulates an OpenGL uniform and provides methods for uploading + * data. It is the callers responsability to ensure the correct sized data is + * written to the uniform. + * + * Note that in the case where ensure_exists is false it is still valid to call + * set_value. + */ +class OpenGLUniform +{ + public: + /** + * Construct a new OpenGLUniform. + * + * @param program + * OpenGL program uniform is for. + * + * @param name + * The name of the uniform. + * + * @param ensure_exists + * If true then an exception will be thrown if the uniform does not + * exists, else construction continues as normal. + */ + OpenGLUniform(GLuint program, const std::string &name, bool ensure_exists = true); + + /** + * Set a matrix value for the uniform. + * + * @param value + * The value to set. + */ + void set_value(const Matrix4 &value) const; + + /** + * Set an array of matrix values for the uniform. + * + * @param value + * The value to set. + */ + void set_value(const std::vector &value) const; + + /** + * Set an array of float values for the uniform. + * + * @param value + * The value to set. + */ + void set_value(const std::array &value) const; + + /** + * Set an array of float values for the uniform. + * + * @param value + * The value to set. + */ + void set_value(const std::array &value) const; + + /** + * Set an integer value for the uniform. + * + * @param value + * The value to set. + */ + void set_value(std::int32_t value) const; + + private: + /** OpenGL location of uniform. */ + GLint location_; +}; + +} diff --git a/include/iris/graphics/opengl/opengl_windows.h b/include/iris/graphics/opengl/opengl_windows.h new file mode 100644 index 00000000..44395b7e --- /dev/null +++ b/include/iris/graphics/opengl/opengl_windows.h @@ -0,0 +1,103 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +/** + * This is an incomplete file and is intended to be included in + * include/opengl/opengl.h + * + * *DO NOT* include this file directly + * + * This file defines various opengl constants and functions required for windows + */ + +#define GL_ARRAY_BUFFER 0x8892 +#define GL_ELEMENT_ARRAY_BUFFER 0x8893 +#define GL_STATIC_DRAW 0x88E4 +#define GL_LINK_STATUS 0x8B82 +#define GL_INFO_LOG_LENGTH 0x8B84 +#define GL_FRAMEBUFFER 0x8D40 +#define GL_COLOR_ATTACHMENT0 0x8CE0 +#define GL_DEPTH_ATTACHMENT 0x8D00 +#define GL_FRAMEBUFFER_COMPLETE 0x8CD5 +#define GL_TEXTURE0 0x84C0 +#define GL_READ_FRAMEBUFFER 0x8CA8 +#define GL_DRAW_FRAMEBUFFER 0x8CA9 +#define GL_FRAGMENT_SHADER 0x8B30 +#define GL_VERTEX_SHADER 0x8B31 +#define GL_COMPILE_STATUS 0x8B81 +#define GL_CLAMP_TO_BORDER 0x812D +#define GL_CLAMP_TO_EDGE 0x812F +#define GL_RGBA16F 0x881A +#define GL_SRGB 0x8C40 +#define GL_SRGB_ALPHA 0x8C42 +#define GL_FRAMEBUFFER_SRGB 0x8DB9 + +#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091 +#define WGL_CONTEXT_MINOR_VERSION_ARB 0x2092 +#define WGL_CONTEXT_PROFILE_MASK_ARB 0x9126 +#define WGL_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001 +#define WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB 0x00000002 +#define WGL_DRAW_TO_WINDOW_ARB 0x2001 +#define WGL_ACCELERATION_ARB 0x2003 +#define WGL_SUPPORT_OPENGL_ARB 0x2010 +#define WGL_DOUBLE_BUFFER_ARB 0x2011 +#define WGL_PIXEL_TYPE_ARB 0x2013 +#define WGL_COLOR_BITS_ARB 0x2014 +#define WGL_DEPTH_BITS_ARB 0x2022 +#define WGL_STENCIL_BITS_ARB 0x2023 +#define WGL_FULL_ACCELERATION_ARB 0x2027 +#define WGL_TYPE_RGBA_ARB 0x202B + +using GLsizeiptr = std::ptrdiff_t; +using GLintptr = std::ptrdiff_t; +using GLchar = char; + +// opengl function pointers +// because windows supports multiple implementations of the opengl functions we +// will need to resolve all these +// by default when we include opengl.h we want all these to be marked extern +// we will then define them all once in a single translation unit where they can +// be resolved +// read comments in opengl.h for more details on the EXTERN macro + +EXTERN void (*glDeleteBuffers)(GLsizei, const GLuint *); +EXTERN void (*glUseProgram)(GLuint); +EXTERN void (*glBindBuffer)(GLenum, GLuint); +EXTERN void (*glGenVertexArrays)(GLsizei, GLuint *); +EXTERN void (*glDeleteVertexArrays)(GLsizei, GLuint *); +EXTERN void (*glBindVertexArray)(GLuint); +EXTERN void (*glEnableVertexAttribArray)(GLuint); +EXTERN void (*glVertexAttribPointer)(GLuint, GLint, GLenum, GLboolean, GLsizei, const void *); +EXTERN void (*glVertexAttribIPointer)(GLuint, GLint, GLenum, GLsizei, const void *); +EXTERN GLuint (*glCreateProgram)(void); +EXTERN void (*glAttachShader)(GLuint, GLuint); +EXTERN void (*glGenBuffers)(GLsizei, GLuint *); +EXTERN void (*glBufferData)(GLenum, GLsizeiptr, const void *, GLenum); +EXTERN void (*glBufferSubData)(GLenum, GLintptr, GLsizeiptr, const void *); +EXTERN void (*glLinkProgram)(GLuint); +EXTERN void (*glGetProgramiv)(GLuint, GLenum, GLint *); +EXTERN void (*glGetProgramInfoLog)(GLuint, GLsizei, GLsizei *, GLchar *); +EXTERN void (*glDeleteProgram)(GLuint); +EXTERN void (*glGenFramebuffers)(GLsizei, GLuint *); +EXTERN void (*glBindFramebuffer)(GLenum, GLuint); +EXTERN void (*glFramebufferTexture2D)(GLenum, GLenum, GLenum, GLuint, GLint); +EXTERN GLenum (*glCheckFramebufferStatus)(GLenum); +EXTERN void (*glDeleteFramebuffers)(GLsizei, const GLuint *); +EXTERN GLint (*glGetUniformLocation)(GLuint, const GLchar *); +EXTERN void (*glUniformMatrix4fv)(GLint, GLsizei, GLboolean, const GLfloat *); +EXTERN void (*glUniform3f)(GLint, GLfloat, GLfloat, GLfloat); +EXTERN void (*glUniform1fv)(GLint, GLsizei, const GLfloat *); +EXTERN void (*glUniform4fv)(GLint, GLsizei, const GLfloat *); +EXTERN void (*glActiveTexture)(GLenum); +EXTERN void (*glUniform1i)(GLint, GLint); +EXTERN void (*glBlitFramebuffer)(GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLint, GLbitfield, GLenum); +EXTERN GLuint (*glCreateShader)(GLenum); +EXTERN void (*glShaderSource)(GLuint, GLsizei, const GLchar **, const GLint *); +EXTERN void (*glCompileShader)(GLuint); +EXTERN void (*glGetShaderiv)(GLuint, GLenum, GLint *); +EXTERN void (*glGetShaderInfoLog)(GLuint, GLsizei, GLsizei *, GLchar *); +EXTERN void (*glDeleteShader)(GLuint); +EXTERN void (*glGenerateMipmap)(GLenum); \ No newline at end of file diff --git a/include/iris/graphics/primitive_type.h b/include/iris/graphics/primitive_type.h new file mode 100644 index 00000000..ddcc142e --- /dev/null +++ b/include/iris/graphics/primitive_type.h @@ -0,0 +1,23 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** + * Enumeration of primitive types. + */ +enum class PrimitiveType : std::uint32_t +{ + LINES, + TRIANGLES +}; + +} diff --git a/include/iris/graphics/render_command.h b/include/iris/graphics/render_command.h new file mode 100644 index 00000000..404fb4fd --- /dev/null +++ b/include/iris/graphics/render_command.h @@ -0,0 +1,222 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "graphics/lights/light.h" +#include "graphics/lights/light_type.h" +#include "graphics/material.h" +#include "graphics/render_command_type.h" +#include "graphics/render_entity.h" +#include "graphics/render_pass.h" +#include "graphics/render_target.h" + +namespace iris +{ + +/** + * A RenderCommand represents an instruction to a Renderer. It is possible to + * create a queue of these commands such that when executed by a Renderer the + * desired output is presented to a Window. It is expected that each Renderer + * implementation interprets these commands in such a way that the output is + * always the same, therefore the command queue is agnostic to the graphics API. + * + * This class is just a grab-bag of pointers to various rendering objects. The + * type will infer which are valid for any given command (can assume nullptr if + * not valid for a given type). + * + * This is an internal class and a queue is built from RenderPass objects by a + * Renderer. + * + * Note also that this class can violate the "correct by construction" paradigm + * in that: + * - It has a zero argument constructor + * - Every member has a getter/setter and no validation is performed against + * the type. + * + * This is acceptable because: + * 1. It's an internal class so the engine is aware of the limitations. + * 2. It makes creating a queue easier as we can construct one and then just + * use the setters to update the fields as we need to. This is apposed to + * having to recreate the full state of each command every time we need a + * new one. + */ +class RenderCommand +{ + public: + /** + * Construct a new RenderCommand. Default type is PASS_START + */ + RenderCommand(); + + /** + * Constructor a new RenderCommand. + * + * @param type + * Command type. + * + * @param render_pass + * Pointer to RenderPass. + * + * @param material + * Pointer to Material. + * + * @param render_entity + * Pointer to RenderEntity. + * + * @param shadow_map + * Pointer to shadow map RenderTarget. + * + * @param light + * Pointer to light. + */ + RenderCommand( + RenderCommandType type, + const RenderPass *render_pass, + const Material *material, + const RenderEntity *render_entity, + const RenderTarget *shadow_map, + const Light *light); + + /** + * Get command type. + * + * @returns + * Command type. + */ + RenderCommandType type() const; + + /** + * Set command type. + * + * @param type + * New command type. + */ + void set_type(RenderCommandType type); + + /** + * Get pointer to RenderPass. + * + * @returns + * Pointer to RenderPass. + */ + const RenderPass *render_pass() const; + + /** + * Set RenderPass. + * + * @param render_pass + * New RenderPass. + */ + void set_render_pass(const RenderPass *render_pass); + + /** + * Get pointer to Material. + * + * @returns + * Pointer to Material. + */ + const Material *material() const; + + /** + * Set Material. + * + * @param material + * New Material. + */ + void set_material(const Material *material); + + /** + * Get pointer to RenderEntity. + * + * @returns + * Pointer to RenderEntity. + */ + const RenderEntity *render_entity() const; + + /** + * Set RenderEntity. + * + * @param render_entity + * New Material. + */ + void set_render_entity(const RenderEntity *render_entity); + + /** + * Get pointer to Light. + * + * @returns + * Pointer to Light. + */ + const Light *light() const; + + /** + * Set Light. + * + * @param light + * New Light. + */ + void set_light(const Light *light); + + /** + * Get pointer to shadow map RenderTarget. + * + * @returns + * Pointer to shadow map RenderTarget. + */ + const RenderTarget *shadow_map() const; + + /** + * Set shadow map RenderTarget. + * + * @param light + * New shadow map RenderTarget. + */ + void set_shadow_map(const RenderTarget *shadow_map); + + /** + * Equality operator. + * + * @param other + * RenderCommand to compare with this. + * + * @returns + * True if objects are equal, otherwise false. + */ + bool operator==(const RenderCommand &other) const; + + /** + * Inequality operator. + * + * @param other + * RenderCommand to compare with this. + * + * @returns + * True if objects are not equal, otherwise false. + */ + bool operator!=(const RenderCommand &other) const; + + private: + /** Command type. */ + RenderCommandType type_; + + /** Pointer to RenderPass for command. */ + const RenderPass *render_pass_; + + /** Pointer to Material for command. */ + const Material *material_; + + /** Pointer to RenderEntity for command. */ + const RenderEntity *render_entity_; + + /** Pointer to shadow map RenderTarget for command. */ + const RenderTarget *shadow_map_; + + /** Pointer to Light for command. */ + const Light *light_; +}; + +} diff --git a/include/iris/graphics/render_command_type.h b/include/iris/graphics/render_command_type.h new file mode 100644 index 00000000..528b60ef --- /dev/null +++ b/include/iris/graphics/render_command_type.h @@ -0,0 +1,26 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** + * Enumeration of possible render command types. + */ +enum class RenderCommandType : std::uint8_t +{ + UPLOAD_TEXTURE, + PASS_START, + DRAW, + PASS_END, + PRESENT +}; + +} \ No newline at end of file diff --git a/include/iris/graphics/render_entity.h b/include/iris/graphics/render_entity.h new file mode 100644 index 00000000..ffedd75e --- /dev/null +++ b/include/iris/graphics/render_entity.h @@ -0,0 +1,244 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "core/camera_type.h" +#include "core/matrix4.h" +#include "core/quaternion.h" +#include "core/transform.h" +#include "graphics/mesh.h" +#include "graphics/primitive_type.h" +#include "graphics/render_graph/render_node.h" +#include "graphics/skeleton.h" +#include "graphics/texture.h" + +namespace iris +{ + +/** + * A renderable entity. + */ +class RenderEntity +{ + public: + /** + * Construct a RenderEntity. + * + * @param mesh + * Mesh to render. + * + * @param position + * Centre of mesh in world space. + * + * @param primitive_type + * Primitive type of underlying mesh. + */ + RenderEntity(Mesh *mesh, const Vector3 &position, PrimitiveType primitive_type = PrimitiveType::TRIANGLES); + + /** + * Construct a RenderEntity. + * + * @param mesh + * Mesh to render. + * + * @param transform + * Transform of entity in world space. + * + * @param primitive_type + * Primitive type of underlying mesh. + */ + RenderEntity(Mesh *mesh, const Transform &transform, PrimitiveType primitive_type = PrimitiveType::TRIANGLES); + + /** + * Construct a RenderEntity. + * + * @param mesh + * Mesh to render. + * + * @param transform + * Transform of entity in world space. + * + * @param skeleton + * Skeleton. + * + * @param primitive_type + * Primitive type of underlying mesh. + */ + RenderEntity( + Mesh *mesh, + const Transform &transform, + Skeleton skeleton, + PrimitiveType primitive_type = PrimitiveType::TRIANGLES); + + RenderEntity(const RenderEntity &) = delete; + RenderEntity &operator=(const RenderEntity &) = delete; + RenderEntity(RenderEntity &&) = default; + RenderEntity &operator=(RenderEntity &&) = default; + + /** + * Get position. + * + * @returns + * Position. + */ + Vector3 position() const; + + /** + * Set the position of the RenderEntity. + * + * @param position + * New position. + */ + void set_position(const Vector3 &position); + + /** + * Get orientation. + * + * @return + * Orientation. + */ + Quaternion orientation() const; + + /** + * Set the orientation of the RenderEntity. + * + * @param orientation + * New orientation. + */ + void set_orientation(const Quaternion &orientation); + + /** + * Set the scale of the RenderEntity. + * + * @param scale + * New scale. + */ + void set_scale(const Vector3 &scale); + + /** + * Get the transformation matrix of the RenderEntity. + * + * @returns + * Transformation matrix. + */ + Matrix4 transform() const; + + /** + * Set transformation matrix. + * + * @param transform + * New transform matrix. + */ + void set_transform(const Matrix4 &transform); + + /** + * Get the transformation matrix for the normals of the RenderEntity. + * + * @returns + * Normal transformation matrix. + */ + Matrix4 normal_transform() const; + + /** + * Get all Mesh for this entity. + * + * @returns + * Mesh. + */ + Mesh *mesh() const; + + /** + * Set Mesh. + * + * @param mesh + * New Mesh. + */ + void set_mesh(Mesh *mesh); + + /** + * Returns whether the object should be rendered as a wireframe. + * + * @returns + * True if should be rendered as a wireframe, false otherwise. + */ + bool should_render_wireframe() const; + + /** + * Sets whether the object should be rendered as a wireframe. + * + * @param wrireframe + * True if should be rendered as a wireframe, false otherwise. + */ + void set_wireframe(const bool wireframe); + + /** + * Get primitive type. + * + * @returns + * Primitive type. + */ + PrimitiveType primitive_type() const; + + /** + * Get reference to skeleton. + * + * @returns + * Reference to skeleton. + */ + Skeleton &skeleton(); + + /** + * Get const reference to skeleton. + * + * @returns + * Reference to skeleton. + */ + const Skeleton &skeleton() const; + + /** + * Can this entity have shadows rendered on it. + * + * @returns + * True if shadows should be rendered, false otherwise. + */ + bool receive_shadow() const; + + /** + * Set whether this object can have shadows rendered on it. + * + * @param receive_shadow + * New receive shadow option. + */ + void set_receive_shadow(bool receive_shadow); + + private: + /** Mesh to render. */ + Mesh *mesh_; + + /** World space transform. */ + Transform transform_; + + /** Normal transformation matrix. */ + Matrix4 normal_; + + /** Whether the object should be rendered as a wireframe. */ + bool wireframe_; + + /** Primitive type. */ + PrimitiveType primitive_type_; + + /** Skeleton. */ + Skeleton skeleton_; + + /** Should object render shadows. */ + bool receive_shadow_; +}; + +} diff --git a/include/iris/graphics/render_graph/arithmetic_node.h b/include/iris/graphics/render_graph/arithmetic_node.h new file mode 100644 index 00000000..54252a02 --- /dev/null +++ b/include/iris/graphics/render_graph/arithmetic_node.h @@ -0,0 +1,105 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/render_graph/node.h" + +namespace iris +{ + +class ShaderCompiler; + +enum class ArithmeticOperator : std::uint8_t +{ + ADD, + SUBTRACT, + MULTIPLY, + DIVIDE, + DOT +}; + +/** + * Implementation of Node which performs an ArithmeticOperator on two input + * Nodes. + * + * The hierarchy of Arithmetic nodes can be used to set operator precedence, + * for example: + * + * ValueNode(3) ------\ + * ArithmeticNode(+) ------\ + * ValueNode(4) ------/ \ + * ArithmeticNode(/) + * ValueNode(5) ------\ / + * ArithmeticNode(+) ------/ + * ValueNode(6) ------/ + * + * Weill evaluate to ((3 + 4) / (5 + 6)) + */ +class ArithmeticNode : public Node +{ + public: + /** + * Create a new ArithmeticNode. + * + * @param value1 + * First input value. + * + * @param value2 + * Second input value. + * + * @param arithmetic_operator + * Operator to apply to value1 and value2. + */ + ArithmeticNode(Node *value1, Node *value2, ArithmeticOperator arithmetic_operator); + + ~ArithmeticNode() override = default; + + /** + * Accept a compiler visitor. + * + * @param compiler + * Compiler to accept. + */ + void accept(ShaderCompiler &compiler) const override; + + /** + * Get value1. + * + * @returns + * value1. + */ + Node *value1() const; + + /** + * Get value2. + * + * @returns + * value2. + */ + Node *value2() const; + + /** + * Get arithmetic operator. + * + * @returns + * Arithmetic operator. + */ + ArithmeticOperator arithmetic_operator() const; + + private: + /** First value. */ + Node *value1_; + + /** Second value. */ + Node *value2_; + + /** Arithmetic operator applied to value1 and value2. */ + ArithmeticOperator arithmetic_operator_; +}; +} diff --git a/include/iris/graphics/render_graph/blur_node.h b/include/iris/graphics/render_graph/blur_node.h new file mode 100644 index 00000000..7f694880 --- /dev/null +++ b/include/iris/graphics/render_graph/blur_node.h @@ -0,0 +1,54 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/render_graph/node.h" +#include "graphics/render_graph/texture_node.h" + +namespace iris +{ +class ShaderCompiler; + +/** + * Implementation of Node that performs a basic blur on an input texture. + */ +class BlurNode : public Node +{ + public: + /** + * Create a new BlurNode + * + * @param input_node + * Texture to blur. + */ + BlurNode(TextureNode *input_node); + + ~BlurNode() override = default; + + /** + * Accept a compiler visitor. + * + * @param compiler + * Compiler to accept. + */ + void accept(ShaderCompiler &compiler) const override; + + /** + * Get input texture. + * + * @returns + * Input texture. + */ + TextureNode *input_node() const; + + private: + /** Input texture. */ + TextureNode *input_node_; +}; +} diff --git a/include/iris/graphics/render_graph/colour_node.h b/include/iris/graphics/render_graph/colour_node.h new file mode 100644 index 00000000..072ab147 --- /dev/null +++ b/include/iris/graphics/render_graph/colour_node.h @@ -0,0 +1,52 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "core/colour.h" +#include "graphics/render_graph/node.h" + +namespace iris +{ +class ShaderCompiler; + +/** + * Implementation of Node which represents a colour. + */ +class ColourNode : public Node +{ + public: + /** + * Create a new ColourNode. + * + * @param colour + * The colour to represent. + */ + ColourNode(const Colour &colour); + + ~ColourNode() override = default; + + /** + * Accept a compiler visitor. + * + * @param compiler + * Compiler to accept. + */ + void accept(ShaderCompiler &compiler) const override; + + /** + * Get colour. + * + * @returns + * Colour. + */ + Colour colour() const; + + private: + /** Colour. */ + Colour colour_; +}; +} diff --git a/include/iris/graphics/render_graph/combine_node.h b/include/iris/graphics/render_graph/combine_node.h new file mode 100644 index 00000000..89d9fe24 --- /dev/null +++ b/include/iris/graphics/render_graph/combine_node.h @@ -0,0 +1,96 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/render_graph/node.h" + +namespace iris +{ +class ShaderCompiler; + +/** + * Implementation of Node which takes four inputs to create a single + * 4-dimensional value. + */ +class CombineNode : public Node +{ + public: + /** + * Create a new CombineNode. + * + * @param value1 + * First value. + * + * @param value2 + * Second value. + * + * @param value3 + * Third value. + * + * @param value4 + * Fourth value. + */ + CombineNode(Node *value1, Node *value2, Node *value3, Node *value4); + + ~CombineNode() override = default; + + /** + * Accept a compiler visitor. + * + * @param compiler + * Compiler to accept. + */ + void accept(ShaderCompiler &compiler) const override; + + /** + * Get first value. + * + * @returns + * First value. + */ + Node *value1() const; + + /** + * Get second value. + * + * @returns + * Second value. + */ + Node *value2() const; + + /** + * Get third value. + * + * @returns + * Third value. + */ + Node *value3() const; + + /** + * Get fourth value. + * + * @returns + * Fourth value. + */ + Node *value4() const; + + private: + /** First value. */ + Node *value1_; + + /** Second value. */ + Node *value2_; + + /** Third value. */ + Node *value3_; + + /** Fourth value. */ + Node *value4_; +}; +} diff --git a/include/iris/graphics/render_graph/component_node.h b/include/iris/graphics/render_graph/component_node.h new file mode 100644 index 00000000..315ae4ea --- /dev/null +++ b/include/iris/graphics/render_graph/component_node.h @@ -0,0 +1,69 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "graphics/render_graph/node.h" + +namespace iris +{ +class ShaderCompiler; + +/** + * Implementation of Node for extracting components from an input_node. + */ +class ComponentNode : public Node +{ + public: + /** + * Create a new ComponentNode. + * + * @param input_node + * Node to get component from. + * + * @param component + * String representation of components, supports swizzling e.g. "x", "xy", + * "rgb". + */ + ComponentNode(Node *input_node, const std::string &component); + + ~ComponentNode() override = default; + + /** + * Accept a compiler visitor. + * + * @param compiler + * Compiler to accept. + */ + void accept(ShaderCompiler &compiler) const override; + + /** + * Get input_node node. + * + * @returns + * Input node. + */ + Node *input_node() const; + + /** + * Get component string. + * + * @returns + * Component string. + */ + std::string component() const; + + private: + /** Input node. */ + Node *input_node_; + + /** Component string. */ + std::string component_; +}; +} diff --git a/include/iris/graphics/render_graph/composite_node.h b/include/iris/graphics/render_graph/composite_node.h new file mode 100644 index 00000000..5ad31c56 --- /dev/null +++ b/include/iris/graphics/render_graph/composite_node.h @@ -0,0 +1,98 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "core/vector3.h" +#include "graphics/render_graph/node.h" + +namespace iris +{ +class ShaderCompiler; + +/** + * Implementation of Node that composites two other nodes together. For each + * fragment colour1 or colour2 is picked based upon which has the closest depth + * value. + */ +class CompositeNode : public Node +{ + public: + /** + * Create a new CompositeNode. + * + * @param colour1 + * First colour node. + * + * @param colour2 + * Second colour node. + * + * @param depth1 + * Depth values for colour1. + * + * @param depth2 + * Depth values for colour2. + */ + CompositeNode(Node *colour1, Node *colour2, Node *depth1, Node *depth2); + + ~CompositeNode() override = default; + + /** + * Accept a compiler visitor. + * + * @param compiler + * Compiler to accept. + */ + void accept(ShaderCompiler &compiler) const override; + + /** + * Get first colour. + * + * @returns + * First colour. + */ + Node *colour1() const; + + /** + * Get second colour. + * + * @returns + * Second colour. + */ + Node *colour2() const; + + /** + * Get first depth. + * + * @returns + * First depth. + */ + Node *depth1() const; + + /** + * Get second depth. + * + * @returns + * Second depth. + */ + Node *depth2() const; + + private: + /** First colour. */ + Node *colour1_; + + /** Second colour. */ + Node *colour2_; + + /** First depth. */ + Node *depth1_; + + /** Second depth. */ + Node *depth2_; +}; +} diff --git a/include/iris/graphics/render_graph/conditional_node.h b/include/iris/graphics/render_graph/conditional_node.h new file mode 100644 index 00000000..40dab5c7 --- /dev/null +++ b/include/iris/graphics/render_graph/conditional_node.h @@ -0,0 +1,124 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/render_graph/node.h" + +namespace iris +{ + +class ShaderCompiler; + +enum class ConditionalOperator : std::uint8_t +{ + GREATER +}; + +/** + * Implementation of Node which performs a conditional operation on two inputs + * and selects one of two outputs based on the result. + * + * This can be summarised as: + * (input1 ConditionalOperator input2) ? output1 : output2 + */ +class ConditionalNode : public Node +{ + public: + /** + * Create a new ConditionalNode. + * + * @param input_value1 + * First input value (left side of operator). + * + * @param input_value2 + * Second input value (right side of operator). + * + * @param output_value1 + * First output value (if conditional is true). + * + * @param output_value2 + * Second output value (if conditional is false). + * + * @param conditional_operator + * Conditional operator to use with inputs. + */ + ConditionalNode( + Node *input_value1, + Node *input_value2, + Node *output_value1, + Node *output_value2, + ConditionalOperator conditional_operator); + + ~ConditionalNode() override = default; + + /** + * Accept a compiler visitor. + * + * @param compiler + * Compiler to accept. + */ + void accept(ShaderCompiler &compiler) const override; + + /** + * Get input_value1. + * + * @returns + * input_value1. + */ + Node *input_value1() const; + + /** + * Get input_value2. + * + * @returns + * input_value2. + */ + Node *input_value2() const; + + /** + * Get output_value1. + * + * @returns + * output_value1. + */ + Node *output_value1() const; + + /** + * Get output_value2. + * + * @returns + * output_value2. + */ + Node *output_value2() const; + + /** + * Get conditional operator. + * + * @returns + * Conditional operator. + */ + ConditionalOperator conditional_operator() const; + + private: + /** First input value. */ + Node *input_value1_; + + /** Second input value. */ + Node *input_value2_; + + /** First output value. */ + Node *output_value1_; + + /** Second output value. */ + Node *output_value2_; + + /** Conditional operator to use with inputs. */ + ConditionalOperator conditional_operator_; +}; +} diff --git a/include/iris/graphics/render_graph/invert_node.h b/include/iris/graphics/render_graph/invert_node.h new file mode 100644 index 00000000..2cc9a4fe --- /dev/null +++ b/include/iris/graphics/render_graph/invert_node.h @@ -0,0 +1,53 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/render_graph/node.h" + +namespace iris +{ +class ShaderCompiler; + +/** + * Implementation of Node which inverts the input nodes colour. + */ +class InvertNode : public Node +{ + public: + /** + * Create a new InvertNode. + * + * @param input_node + * Node to invert. + */ + InvertNode(Node *input_node); + + ~InvertNode() override = default; + + /** + * Accept a compiler visitor. + * + * @param compiler + * Compiler to accept. + */ + void accept(ShaderCompiler &compiler) const override; + + /** + * Get input node. + * + * @returns + * Input node. + */ + Node *input_node() const; + + private: + /** Input node. */ + Node *input_node_; +}; +} diff --git a/include/iris/graphics/render_graph/node.h b/include/iris/graphics/render_graph/node.h new file mode 100644 index 00000000..4966aef5 --- /dev/null +++ b/include/iris/graphics/render_graph/node.h @@ -0,0 +1,33 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace iris +{ + +// forward declaration +class ShaderCompiler; + +/** + * This is an interface for Node, which forms part of a render graph. Node + * implementations can be connected together to describe whats a shader should + * do. + */ +class Node +{ + public: + virtual ~Node() = default; + + /** + * Accept a compiler visitor. + * + * @param compiler + * Compiler to accept. + */ + virtual void accept(ShaderCompiler &compiler) const = 0; +}; +} diff --git a/include/iris/graphics/render_graph/post_processing_node.h b/include/iris/graphics/render_graph/post_processing_node.h new file mode 100644 index 00000000..99c930aa --- /dev/null +++ b/include/iris/graphics/render_graph/post_processing_node.h @@ -0,0 +1,50 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#pragma once + +#include +#include + +#include "graphics/render_graph/render_node.h" + +namespace iris +{ + +class ShaderCompiler; + +/** + * Specialisation of RenderNode for applying post processing effects such as: + * - Tone mapping + * - Gamma correction + * + * Note that this is automatically added by the engine. + */ +class PostProcessingNode : public RenderNode +{ + public: + /** + * Create a new PostProcessingNode. + * + * @param input + * Colour input for RenderNide. + */ + PostProcessingNode(Node *input); + + ~PostProcessingNode() override = default; + + /** + * Accept a compiler visitor. + * + * @param compiler + * Compiler to accept. + */ + void accept(ShaderCompiler &compiler) const override; +}; + +} \ No newline at end of file diff --git a/include/iris/graphics/render_graph/render_graph.h b/include/iris/graphics/render_graph/render_graph.h new file mode 100644 index 00000000..8badbaa2 --- /dev/null +++ b/include/iris/graphics/render_graph/render_graph.h @@ -0,0 +1,93 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "graphics/render_graph/node.h" +#include "graphics/render_graph/render_node.h" + +namespace iris +{ + +/** + * This class encapsulates a render graph - a series of connected nodes that + * can be compiled into API specific shaders. + * + * This class automatically creates and managed a RenderNode. + */ +class RenderGraph +{ + public: + // helper trait + template + using is_node = std::enable_if_t && !std::is_same_v>; + + /** + * Create a new RenderGraph. + */ + RenderGraph(); + + /** + * Get the RenderNode i.e. the root of the graph. + * + * @returns + * Render node. + */ + RenderNode *render_node() const; + + /** + * Create a Node and add it to the graph. Uses perfect forwarding to pass + * along arguments. + * + * @param args + * Arguments for Node. + * + * @returns + * A pointer to the newly created Node. + */ + template > + T *create(Args &&...args) + { + auto node = std::make_unique(std::forward(args)...); + return static_cast(add(std::move(node))); + } + + /** + * Set the render node. + * + * @param args + * Arguments for Node. + * + * @returns + * A pointer to the newly created Node. + */ + template > + RenderNode *set_render_node(Args &&...args) + { + nodes_[0] = std::make_unique(std::forward(args)...); + return render_node(); + } + + /** + * Add a Node to the graph. + * + * @param node + * Node to add. + * + * @returns + * Pointer to the added node. + */ + Node *add(std::unique_ptr node); + + private: + /** Collection of nodes in graph. */ + std::vector> nodes_; +}; +} diff --git a/include/iris/graphics/render_graph/render_node.h b/include/iris/graphics/render_graph/render_node.h new file mode 100644 index 00000000..04b3e7d1 --- /dev/null +++ b/include/iris/graphics/render_graph/render_node.h @@ -0,0 +1,182 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "graphics/render_graph/node.h" + +namespace iris +{ + +class ShaderCompiler; + +/** + * A RenderNode is an implementation of Node which represents the final node in + * a render graph. Whilst it is the final node it is effectively the root of the + * DAG and the whole graph can be traversed top-down from here. Only one should + * be in each render_graph. + */ +class RenderNode : public Node +{ + public: + /** + * Construct a new RenderNode. All input nodes are initialised to nullptr, + * which tells the compiler to default to values in the vertex data. + */ + RenderNode(); + + ~RenderNode() override = default; + + /** + * Accept a compiler visitor. + * + * @param compiler + * Compiler to accept. + */ + void accept(ShaderCompiler &compiler) const override; + + /** + * Get colour input. + * + * @returns + * Colour input. + */ + Node *colour_input() const; + + /** + * Set colour input. + * + * @param input + * New input. + */ + void set_colour_input(Node *input); + + /** + * Set specular power input i.e. how shiny an object is. + * + * @returns + * Specular power input. + */ + Node *specular_power_input() const; + + /** + * Set specular power input. + * + * @param input + * New input. + */ + void set_specular_power_input(Node *input); + + /** + * Set specular amount input i.e. a scale from [0, 1] how much to apply the + * specular power. Useful for specular maps or to disable specular. + * + * @returns + * Specular amount input. + */ + Node *specular_amount_input() const; + + /** + * Set specular amount input. + * + * @param input + * New input. + */ + void set_specular_amount_input(Node *input); + + /** + * Get normal input. + * + * @returns + * Colour input. + */ + Node *normal_input() const; + + /** + * Set normal input. + * + * @param input + * New input. + */ + void set_normal_input(Node *input); + + /** + * Get vertex position input. + * + * @returns + * Colour input. + */ + Node *position_input() const; + + /** + * Set vertex position input. + * + * @param input + * New input. + */ + void set_position_input(Node *input); + + /** + * Get the shadow map input node at a specified index. + * + * @param index + * Index of shadow map input. + * + * @returns + * Shadow map node at index if one exists, otherwise nullptr. + */ + Node *shadow_map_input() const; + + /** + * Add a new shadow map node. + * + * @param input + * Shadow map input node. + */ + void set_shadow_map_input(Node *input); + + /** + * Is this render node a depth only render. + * + * @returns + * True if depth only render, otherwise false. + */ + bool is_depth_only() const; + + /** + * Set if this node is for a depth only render. + * + * @param depth_only + * New depth only value. + */ + void set_depth_only(bool depth_only); + + private: + /** Colour input. */ + Node *colour_input_; + + /** Specular power input. */ + Node *specular_power_input_; + + /** Specular amount input. */ + Node *specular_amount_input_; + + /** Normal input. */ + Node *normal_input_; + + /** Vertex position input. */ + Node *position_input_; + + /** Collection of shadow map inputs. */ + Node *shadow_map_input_; + + /** Is depth only render. */ + bool depth_only_; +}; +} diff --git a/include/iris/graphics/render_graph/shader_compiler.h b/include/iris/graphics/render_graph/shader_compiler.h new file mode 100644 index 00000000..aae2d4f9 --- /dev/null +++ b/include/iris/graphics/render_graph/shader_compiler.h @@ -0,0 +1,93 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "core/colour.h" +#include "core/vector3.h" +#include "graphics/lights/light_type.h" +#include "graphics/texture.h" + +namespace iris +{ + +class RenderNode; +class PostProcessingNode; +class ColourNode; +class TextureNode; +class InvertNode; +class BlurNode; +class CompositeNode; +class VertexPositionNode; +class ArithmeticNode; +class ConditionalNode; +class ComponentNode; +class CombineNode; +class SinNode; +template +class ValueNode; + +/** + * Interface for a class that compiles a RenderGraph into API specific shaders. + * This treats the RenderGraph as an AST and parses it top-down. + */ +class ShaderCompiler +{ + public: + virtual ~ShaderCompiler() = default; + + // visitor methods + virtual void visit(const RenderNode &node) = 0; + virtual void visit(const PostProcessingNode &node) = 0; + virtual void visit(const ColourNode &node) = 0; + virtual void visit(const TextureNode &node) = 0; + virtual void visit(const InvertNode &node) = 0; + virtual void visit(const BlurNode &node) = 0; + virtual void visit(const CompositeNode &node) = 0; + virtual void visit(const VertexPositionNode &node) = 0; + virtual void visit(const ValueNode &node) = 0; + virtual void visit(const ValueNode &node) = 0; + virtual void visit(const ValueNode &node) = 0; + virtual void visit(const ArithmeticNode &node) = 0; + virtual void visit(const ConditionalNode &node) = 0; + virtual void visit(const ComponentNode &node) = 0; + virtual void visit(const CombineNode &node) = 0; + virtual void visit(const SinNode &node) = 0; + + /** + * Get the compiled vertex shader. + * + * Compiled here means from the render graph to a string, not to an API + * specific object on the hardware. + * + * @returns + * Vertex shader. + */ + virtual std::string vertex_shader() const = 0; + + /** + * Get the compiled fragment shader. + * + * Compiled here means from the render graph to a string, not to an API + * specific object on the hardware. + * + * @returns + * Fragment shader. + */ + virtual std::string fragment_shader() const = 0; + + /** + * Collection of textures needed for the shaders. + * + * @returns + * Collection of Textures. + */ + virtual std::vector textures() const = 0; +}; +} \ No newline at end of file diff --git a/include/iris/graphics/render_graph/sin_node.h b/include/iris/graphics/render_graph/sin_node.h new file mode 100644 index 00000000..a7440168 --- /dev/null +++ b/include/iris/graphics/render_graph/sin_node.h @@ -0,0 +1,53 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/render_graph/node.h" + +namespace iris +{ +class ShaderCompiler; + +/** + * Implementation of Node which calculates the sine of the input node. + */ +class SinNode : public Node +{ + public: + /** + * Create a new SinNode. + * + * @param input_node + * Node to sine. + */ + SinNode(Node *input_node); + + ~SinNode() override = default; + + /** + * Accept a compiler visitor. + * + * @param compiler + * Compiler to accept. + */ + void accept(ShaderCompiler &compiler) const override; + + /** + * Get input node. + * + * @returns + * Input node. + */ + Node *input_node() const; + + private: + /** Input node. */ + Node *input_node_; +}; +} diff --git a/include/iris/graphics/render_graph/texture_node.h b/include/iris/graphics/render_graph/texture_node.h new file mode 100644 index 00000000..fa93c5bb --- /dev/null +++ b/include/iris/graphics/render_graph/texture_node.h @@ -0,0 +1,64 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "core/vector3.h" +#include "graphics/render_graph/node.h" +#include "graphics/texture.h" +#include "graphics/texture_usage.h" + +namespace iris +{ +class ShaderCompiler; + +/** + * Implementation of Node which provides access to a texture. The compiler will + * sample this texture for the current fragments UV, using this as input to + * another node will produce a four float value (RGBA). + */ +class TextureNode : public Node +{ + public: + /** + * Create a new TextureNode. + * + * @param texture + * Texture to provide access to. + */ + TextureNode(Texture *texture); + + /** + * Create a new TextureNode. + * + * @param path + * Path of texture. + */ + TextureNode(const std::string &path, TextureUsage usage = TextureUsage::IMAGE); + + ~TextureNode() override = default; + + /** + * Accept a compiler visitor. + * + * @param compiler + * Compiler to accept. + */ + void accept(ShaderCompiler &compiler) const override; + + /** + * Get texture. + * + * @returns + * Texture. + */ + Texture *texture() const; + + private: + /** Texture. */ + Texture *texture_; +}; +} diff --git a/include/iris/graphics/render_graph/value_node.h b/include/iris/graphics/render_graph/value_node.h new file mode 100644 index 00000000..5989f560 --- /dev/null +++ b/include/iris/graphics/render_graph/value_node.h @@ -0,0 +1,61 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "graphics/render_graph/node.h" +#include "graphics/render_graph/shader_compiler.h" + +namespace iris +{ +/** + * Implementation of Node which provides access to a constant value. See + * compiler.h for supported types. + */ +template +class ValueNode : public Node +{ + public: + /** + * Create a new ValueNode. + * + * @param value + * Value to provide access to. + */ + ValueNode(const T &value) + : value_(value) + { + } + + ~ValueNode() override = default; + + /** + * Accept a compiler visitor. + * + * @param compiler + * Compiler to accept. + */ + void accept(ShaderCompiler &compiler) const override + { + return compiler.visit(*this); + } + + /** + * Get value. + * + * @returns + * Value. + */ + T value() const + { + return value_; + } + + private: + /** Value. */ + T value_; +}; +} diff --git a/include/iris/graphics/render_graph/vertex_position_node.h b/include/iris/graphics/render_graph/vertex_position_node.h new file mode 100644 index 00000000..cb3408cb --- /dev/null +++ b/include/iris/graphics/render_graph/vertex_position_node.h @@ -0,0 +1,35 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "graphics/render_graph/node.h" + +namespace iris +{ +class ShaderCompiler; + +/** + * Implementation of Node which provides access to a meshes vertex position. + */ +class VertexPositionNode : public Node +{ + public: + /** + * Create a new VertexPositionNode. + */ + VertexPositionNode(); + ~VertexPositionNode() override = default; + + /** + * Accept a compiler visitor. + * + * @param compiler + * Compiler to accept. + */ + void accept(ShaderCompiler &compiler) const override; +}; +} diff --git a/include/iris/graphics/render_pass.h b/include/iris/graphics/render_pass.h new file mode 100644 index 00000000..22a0a618 --- /dev/null +++ b/include/iris/graphics/render_pass.h @@ -0,0 +1,61 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "core/camera.h" +#include "graphics/render_target.h" +#include "graphics/scene.h" + +namespace iris +{ + +/** + * Struct encapsulating the high-level engin objects needed for a render pass. + * This describes: + * - what to render + * - where to render it from + * - where to render it to + * + * It is an engine convention that a nullptr target means render to the default + * target i.e. the window. + */ +struct RenderPass +{ + /** + * Create a new RenderPass. + * + * @param scene + * The scene to render. + * + * @param camera + * The camera to render from + * + * @param target + * The target to render to, nullptr means the default window target.c + */ + RenderPass(Scene *scene, Camera *camera, RenderTarget *target) + : scene(scene) + , camera(camera) + , render_target(target) + , depth_only(false) + { + } + + /** Scene to render */ + const Scene *scene; + + /** Camera to render with. */ + const Camera *camera; + + /** Target to render to. */ + const RenderTarget *render_target; + + /** Flag indicating that only depth information should be rendered. */ + bool depth_only = false; +}; + +} diff --git a/include/iris/graphics/render_queue_builder.h b/include/iris/graphics/render_queue_builder.h new file mode 100644 index 00000000..9ec66ae4 --- /dev/null +++ b/include/iris/graphics/render_queue_builder.h @@ -0,0 +1,72 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "graphics/lights/light_type.h" +#include "graphics/render_entity.h" +#include "graphics/render_graph/render_graph.h" +#include "graphics/render_target.h" +#include "graphics/renderer.h" +#include "graphics/texture.h" + +namespace iris +{ + +/** + * This class provides a generic way of building a render queue (i.e. a + * collection of RenderCommand objects) from a collection of RenderPass objects. + * + * It is decoupled from any graphics API by requiring callbacks for graphics + * specific objects. + */ +class RenderQueueBuilder +{ + public: + // aliases for callbacks + using CreateMaterialCallback = + std::function; + using CreateRenderTargetCallback = std::function; + + /** + * Construct a new RenderQueueBuilder + * + * @param create_material_callback + * Callback fro creating a Material object. + * + * @param create_render_target_callback + * Callback for creating a RenderTarget object. + */ + RenderQueueBuilder( + CreateMaterialCallback create_material_callback, + CreateRenderTargetCallback create_render_target_callback); + + /** + * Build a render queue (a collection of RenderCommand objects) from a + * collection of RenderPass objects. + * + * @param render_passes + * RenderPass objects to create a render queue from. + * + * @returns + * Collection of RenderCommand objects which when executed will render the + * supplied passes. + */ + std::vector build(std::vector &render_passes) const; + + private: + /** Callback fro creating a Material object. */ + CreateMaterialCallback create_material_callback_; + + /** Callback for creating a RenderTarget object. */ + CreateRenderTargetCallback create_render_target_callback_; +}; + +} diff --git a/include/iris/graphics/render_target.h b/include/iris/graphics/render_target.h new file mode 100644 index 00000000..e6d0a119 --- /dev/null +++ b/include/iris/graphics/render_target.h @@ -0,0 +1,82 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "graphics/texture.h" + +namespace iris +{ + +/** + * Abstract class for a RenderTarget - a class that encapsulates something the + * renderer can render to. It also provides access to the colour and depth + * Texture objects. + * + * Note that you cannot access the colour or depth data directly after a render + * as it is not synchronised back to the CPU, you can use those textures as + * inputs for other rendering operations. + */ +class RenderTarget +{ + public: + /** + * Create a new RenderTarget. + * + * @param colour_texture + * Texture to render colour data to. + * + * @param depth_texture + * Texture to render depth data to. + */ + RenderTarget(std::unique_ptr colour_texture, std::unique_ptr depth_texture); + + virtual ~RenderTarget() = 0; + + /** + * Get a pointer to the texture storing the target colour data. + * + * @returns + * Colour texture. + */ + Texture *colour_texture() const; + + /** + * Get a pointer to the texture storing the target depth data. + * + * @returns + * Depth texture. + */ + Texture *depth_texture() const; + + /** + * Get the width of the RenderTarget. + * + * @returns + * Render target width. + */ + std::uint32_t width() const; + + /** + * Get the height of the RenderTarget. + * + * @returns + * Render target height. + */ + std::uint32_t height() const; + + protected: + /** Colour texture. */ + std::unique_ptr colour_texture_; + + /** Depth texture. */ + std::unique_ptr depth_texture_; +}; + +} diff --git a/include/iris/graphics/renderer.h b/include/iris/graphics/renderer.h new file mode 100644 index 00000000..e9d28e63 --- /dev/null +++ b/include/iris/graphics/renderer.h @@ -0,0 +1,93 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "core/camera.h" +#include "graphics/lights/light_type.h" +#include "graphics/render_command.h" +#include "graphics/render_pass.h" +#include "graphics/render_target.h" +#include "graphics/scene.h" + +#include + +namespace iris +{ + +/** + * Interface for a Renderer - a class that executes a collection of RenderPass + * objects. + */ +class Renderer +{ + public: + Renderer(); + virtual ~Renderer() = default; + + /** + * Render the current RenderPass objects. + */ + virtual void render(); + + /** + * Set the render passes. These will be executed when render() is called. + * + * @param render_passes + * Collection of RenderPass objects to render. + */ + virtual void set_render_passes(const std::vector &render_passes) = 0; + + /** + * Create a RenderTarget with custom dimensions. + * + * @param width + * Width of render target. + * + * @param height + * Height of render target. + * + * @returns + * RenderTarget. + */ + virtual RenderTarget *create_render_target(std::uint32_t width, std::uint32_t height) = 0; + + protected: + // these functions provide implementors a chance to handle each + // RenderCommandType, where each function below corresponds to one of the + // enum types - with the addition of pre_render and post_render which get + // called before and after each frame + // defaults are all no-ops so implementations only need to override the + // ones needed for their graphics api + + virtual void pre_render(); + virtual void execute_upload_texture(RenderCommand &command); + virtual void execute_pass_start(RenderCommand &command); + virtual void execute_draw(RenderCommand &command); + virtual void execute_pass_end(RenderCommand &command); + virtual void execute_present(RenderCommand &command); + virtual void post_render(); + + /** The collection of RenderPass objects to be rendered. */ + std::vector render_passes_; + + /** + * The queue of RenderCommand objects created from the current RenderPass + * objects. + * */ + std::vector render_queue_; + + /** Scene for the post processing step. */ + std::unique_ptr post_processing_scene_; + + /** RenderTarget for the post processing step. */ + RenderTarget *post_processing_target_; + + /** Camera for the post processing step. */ + std::unique_ptr post_processing_camera_; +}; + +} diff --git a/include/iris/graphics/scene.h b/include/iris/graphics/scene.h new file mode 100644 index 00000000..3fdf81f9 --- /dev/null +++ b/include/iris/graphics/scene.h @@ -0,0 +1,192 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "graphics/lights/lighting_rig.h" +#include "graphics/render_entity.h" +#include "graphics/render_graph/render_graph.h" + +namespace iris +{ + +/** + * A scene is a collection of entities to be rendered. It owns the memory of its + * render entities. + */ +class Scene +{ + public: + /** + * Create a new Scene. + */ + Scene(); + + /** + * Create a RenderGraph for use in this scene. Uses perfect forwarding to + * pass along all arguments. + * + * @param args + * Arguments for RenderGraph. + * + * @returns + * Pointer to the newly created RenderGraph. + */ + template + RenderGraph *create_render_graph(Args &&...args) + { + auto graph = std::make_unique(std::forward(args)...); + return add(std::move(graph)); + } + + /** + * Add a RenderGraph for use in this scene. + * + * @param graph + * RenderGraph to add. + * + * @returns + * Pointer to newly added RenderGraph. + */ + RenderGraph *add(std::unique_ptr graph); + + /** + * Create a RenderEntity and add it to the scene. Uses perfect forwarding to + * pass along all arguments. + * + * @param render_graph + * RenderGraph for RenderEntity. + * + * @param args + * Arguments for RenderEntity. + * + * @returns + * Pointer to the newly created RenderEntity. + */ + template + RenderEntity *create_entity(RenderGraph *render_graph, Args &&...args) + { + auto element = std::make_unique(std::forward(args)...); + + return add(std::move(render_graph), std::move(element)); + } + + /** + * Add a RenderEntity to the scene. + * + * @param render_graph + * RenderGraph for RenderEntity. + * + * @param entity + * RenderEntity to add to scene. + * + * @returns + * Pointer to the added RenderEntity. + */ + RenderEntity *add(RenderGraph *render_graph, std::unique_ptr entity); + + void remove(RenderEntity *entity); + + /** + * Create a Light and add it to the scene. Uses perfect forwarding to pass + * along all arguments. + * + * @param args + * Args for light. + * + * @returns + * Pointer to newly created light. + */ + template + T *create_light(Args &&...args) + { + auto light = std::make_unique(std::forward(args)...); + return add(std::move(light)); + } + + /** + * Add a point light to the scene. + * + * @param light + * Light to add to scene + * + * @returns + * Pointer to the added light. + */ + PointLight *add(std::unique_ptr light); + + /** + * Add a directional light to the scene. + * + * @param light + * Light to add to scene + * + * @returns + * Pointer to the added light. + */ + DirectionalLight *add(std::unique_ptr light); + + /** + * Get ambient light colour. + * + * @returns + * Ambient light colour. + */ + Colour ambient_light() const; + + /** + * Set ambient light colour. + * + * @param colour + * New ambient light colour. + */ + void set_ambient_light(const Colour &colour); + + /** + * Get the RenderGraph for a given RenderEntity. + * + * @param entity + * RenderEntity to get RenderGraph for. + * + * @returns + * RenderGraph for supplied RenderEntity. + */ + RenderGraph *render_graph(RenderEntity *entity) const; + + /** + * Get a reference to all entities in the scene. + * + * @returns + * Collection of tuples. + */ + std::vector>> &entities(); + + const std::vector>> &entities() const; + + /** + * Get LightingRig. + * + * @returns + * Pointer to LightingRig. + */ + const LightingRig *lighting_rig() const; + + private: + /** Collection of tuples. */ + std::vector>> entities_; + + /** Collection of RenderGraphs. */ + std::vector> render_graphs_; + + /** Lighting rig for scene. */ + LightingRig lighting_rig_; +}; + +} diff --git a/include/iris/graphics/shader_type.h b/include/iris/graphics/shader_type.h new file mode 100644 index 00000000..4c26eaef --- /dev/null +++ b/include/iris/graphics/shader_type.h @@ -0,0 +1,23 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** + * Enumeration of shader types. + */ +enum ShaderType : std::uint32_t +{ + VERTEX, + FRAGMENT +}; + +} diff --git a/include/iris/graphics/skeleton.h b/include/iris/graphics/skeleton.h new file mode 100644 index 00000000..1eeb4596 --- /dev/null +++ b/include/iris/graphics/skeleton.h @@ -0,0 +1,176 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "core/matrix4.h" +#include "core/transform.h" +#include "graphics/animation.h" +#include "graphics/bone.h" + +namespace iris +{ + +/** + * A skeleton provides an interface for animating bones. + * + * Internally bones are ordered into a tree hierarchy, setting an animation will + * then walk the tree, applying transformations to bones and its children. + * + * A skeleton can only have a single root note (i.e. one without a parent). + * + * The internal order of bones is important (and may be different to the input + * order), therefore an interface is also provided for querying bone index. + * + * The following diagram shows a skeleton and the bone hierarchy. + * + * +---+ + * | O | ---------- head + * . +---+ + * | . | ---------- neck + * +---+ + * +---+ +---+ +---+ + * left_arm --- | /| | | | |\ .| --- right_arm + * | / | | | | | \| + * +---+ | | | +---+ + * | | | + * | | | --------- spine + * +---+ + * | . | --------- hip + * +---+ + * +---+ +---+ + * left_leg --- | /| |\ .| --- right_leg + * | / | | \| + * +---+ +---+ + * spine (root) + * | + * +--- neck + * | | + * | '--- head + * | + * '--- right_arm + * | + * '--- left_arm + * | + * '--- hip + * | + * '--- right_leg + * | + * '--- left_leg + * + */ +class Skeleton +{ + public: + /** + * Construct an empty skeleton. + */ + Skeleton(); + + /** + * Construct a skeleton. + * + * @param bones + * Collection of bones, these will be reordered. + * + * @parma animations + * Collection of animations for supplied bones + */ + Skeleton(std::vector bones, const std::vector &animations); + + /** + * Get reference to collection of bones. + * + * @returns + * Reference to bone collection. + */ + const std::vector &bones() const; + + /** + * Get reference to collection of transformation matrices for all bones (for + * current animation). This is suitable for transferring to GPU. + * + * @returns + * Reference to bone transformations. + */ + const std::vector &transforms() const; + + /** + * Set the animation. Will reset animation time. + * + * @param name + * Name of animation. + */ + void set_animation(const std::string &name); + + /** + * Get reference to current animation. + * + * @returns + * Animation reference. + */ + Animation &animation(); + + /** + * Advance animation time. See Animation::advance(). + */ + void advance(); + + /** + * Get the index of the given bone name. + * + * @param name + * Name of bone. + * + * @returns + * Index of bone. + */ + std::size_t bone_index(const std::string &name) const; + + /** + * Get reference to bone at index. + * + * @param index + * Index of bone to get. + * + * @returns + * Reference to bone. + */ + Bone &bone(std::size_t index); + + /** + * Get const reference to bone at index. + * + * @param index + * Index of bone to get. + * + * @returns + * Const reference to bone. + */ + const Bone &bone(std::size_t index) const; + + private: + /** Collection of bones, in hierarchical order. */ + std::vector bones_; + + /** Index of parents for bones. */ + std::vector parents_; + + /** Collection of transform matrices for bones. */ + std::vector transforms_; + + /** Collection of animations. */ + std::vector animations_; + + /** Current animation. */ + std::string current_animation_; +}; + +} diff --git a/include/iris/graphics/text_factory.h b/include/iris/graphics/text_factory.h new file mode 100644 index 00000000..dad58d41 --- /dev/null +++ b/include/iris/graphics/text_factory.h @@ -0,0 +1,39 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +#include "core/colour.h" +#include "graphics/texture.h" + +namespace iris::text_factory +{ + +/** + * Construct a new font. + * + * @param font_name + * The name of a Font to load. This is located and loaded in a + * platform specific way, so the Font must exist for the current + * platform. + * + * @param size + * The Font size. + * + * @param text + * The text to render. + * + * @param colour + * The colour of the font. + */ +Texture *create(const std::string &font_name, std::uint32_t size, const std::string &text, const Colour &colour); + +} diff --git a/include/iris/graphics/texture.h b/include/iris/graphics/texture.h new file mode 100644 index 00000000..9b2fa487 --- /dev/null +++ b/include/iris/graphics/texture.h @@ -0,0 +1,110 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "core/data_buffer.h" +#include "graphics/texture_usage.h" + +namespace iris +{ + +/** + * Abstract class that encapsulates a renderable texture. + */ +class Texture +{ + public: + /** + * Creates a new Texture with custom data. + * + * @param data + * Raw data of image, in pixel_format. + * + * @param width + * Width of image. + * + * @param height + * Height of image. + * + * @param pixel_format + * Pixel format. + */ + Texture(const DataBuffer &data, std::uint32_t width, std::uint32_t height, TextureUsage usage); + + virtual ~Texture() = 0; + + Texture(const Texture &) = delete; + Texture &operator=(const Texture &) = delete; + + /** + * Get the raw image data. + * + * @returns + * Raw image data. + */ + const DataBuffer &data() const; + + /** + * Get the width of the image. + * + * @returns + * Image width. + */ + std::uint32_t width() const; + + /** + * Get the height of the image. + * + * @returns + * Image height. + */ + std::uint32_t height() const; + + /** + * Get the texture usage. + * + * @returns + * Texture usage. + */ + TextureUsage usage() const; + + /** + * Should a texture be flipped vertically. + * + * @returns + * Should flip. + */ + bool flip() const; + + /** + * Set whether texture should be flipped vertically. + * + * @param flip + * New flip value. + */ + void set_flip(bool flip); + + protected: + /** Raw image data. */ + DataBuffer data_; + + /** Image width. */ + std::uint32_t width_; + + /** Image height. */ + std::uint32_t height_; + + /** Should texture be flipped vertically. */ + bool flip_; + + /** Texture usage. */ + TextureUsage usage_; +}; + +} diff --git a/include/iris/graphics/texture_manager.h b/include/iris/graphics/texture_manager.h new file mode 100644 index 00000000..b1948342 --- /dev/null +++ b/include/iris/graphics/texture_manager.h @@ -0,0 +1,137 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include + +#include "core/data_buffer.h" +#include "graphics/texture.h" +#include "graphics/texture_usage.h" + +namespace iris +{ + +/** + * Abstract class for creating and managing Texture objects. This class handles + * caching and lifetime management of all created objects. Implementers just + * need to provide a graphics API specific method for creating Texture objects. + */ +class TextureManager +{ + public: + virtual ~TextureManager() = default; + + /** + * Load a texture from the supplied file. Will use ResourceManager. + * + * This function uses caching, so loading the same resource more than once + * will return the same handle. + * + * @param resource + * File to load. + * + * @param usage + * The usage of the texture. Default is IMAGE i.e. something that will be + * rendered. If Texture represents something like a normal or height map + * the DATA should be used. + * + * @returns + * Pointer to loaded texture. + */ + Texture *load(const std::string &resource, TextureUsage usage = TextureUsage::IMAGE); + + /** + * Create a texture from a DataBuffer. + * + * @param data + * Raw data of image, in pixel_format. + * + * @param width + * Width of image. + * + * @param height + * Height of image. + * + * @param usage + * The usage of the texture. + */ + Texture *create(const DataBuffer &data, std::uint32_t width, std::uint32_t height, TextureUsage usage); + + /** + * Unloaded the supplied texture (if there are no other references to it). + * + * Normally we would want textures to stay loaded to avoid excess loads. + * However in some cases it may be necessary to unload a texture (if we know + * we don't want to use it again). + * + * This function decrements an internal reference count and will only + * actually unload texture memory if that reference count reaches 0. + * + * @param texture + * Texture to unload. + */ + void unload(Texture *texture); + + /** + * Get a blank 1x1 white texture + * + * @returns + * Blank texture. + */ + Texture *blank(); + + protected: + /** + * Create a Texture object with the provided data. + * + * @param data + * Raw data of image, in pixel_format. + * + * @param width + * Width of image. + * + * @param height + * Height of image. + * + * @param usage + * Usage of the texture. + */ + virtual std::unique_ptr do_create( + const DataBuffer &data, + std::uint32_t width, + std::uint32_t height, + TextureUsage usage) = 0; + + /** + * Implementors should override this method to provide implementation + * specific unloading logic. Called automatically when a Texture is being + * unloaded (after its reference count is zero), default is a no-op. + * + * @param texture + * Texture about to be unloaded. + */ + virtual void destroy(Texture *texture); + + private: + /** + * Support struct to store a loaded Texture and a reference count. + */ + struct LoadedTexture + { + std::size_t ref_count; + std::unique_ptr texture; + }; + + /** Collection of loaded textures. */ + std::unordered_map loaded_textures_; +}; + +} diff --git a/include/iris/graphics/texture_usage.h b/include/iris/graphics/texture_usage.h new file mode 100644 index 00000000..86254083 --- /dev/null +++ b/include/iris/graphics/texture_usage.h @@ -0,0 +1,25 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** + * Encapsulation of the semantic usage of textures. + */ +enum class TextureUsage : std::uint8_t +{ + IMAGE, + DATA, + RENDER_TARGET, + DEPTH +}; + +} diff --git a/include/iris/graphics/vertex_attributes.h b/include/iris/graphics/vertex_attributes.h new file mode 100644 index 00000000..a36781aa --- /dev/null +++ b/include/iris/graphics/vertex_attributes.h @@ -0,0 +1,118 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "core/vector3.h" + +namespace iris +{ + +/** + * Enumeration of attribute types. + */ +enum class VertexAttributeType +{ + FLOAT_3, + FLOAT_4, + UINT32_1, + UINT32_4 +}; + +/** + * Struct encapsulating data needed to describe a vertex attribute. + */ +struct VertexAttribute +{ + /** Attribute type. */ + VertexAttributeType type; + + /** Number of components in attribute, typically 1, 2, 3 or 4. */ + std::size_t components; + + /** Size of attribute (size(type) * components). */ + std::size_t size; + + /** Number of bytes from start of attribute collection. */ + std::size_t offset; +}; + +/** + * Class encapsulating a series of vertex attributes. These describe how to + * interpret a vertex buffer, which is just a series of bytes. + */ +class VertexAttributes +{ + public: + // useful aliases + using attributes = std::vector; + using const_iterator = attributes::const_iterator; + + /** + * Construct a new VertexAttributes. + * + * @param attributes + * Collection of attributes. + */ + VertexAttributes(const std::vector &attributes); + + /** + * Get size of all attributes. This will be the same as the size of a buffer + * containing all vertex data. + * + * @returns + * Size of attributes. + */ + std::size_t size() const; + + /** + * Get iterator to start if attributes. + * + * Note that this class is immutable so this returns a const_iterator. + * + * @returns + * Iterator to start of attributes. + */ + const_iterator begin() const; + + /** + * Get iterator to end if attributes. + * + * Note that this class is immutable so this returns a const_iterator. + * + * @returns + * Iterator to end of attributes. + */ + const_iterator end() const; + + /** + * Get iterator to start if attributes. + * + * @returns + * Iterator to start of attributes. + */ + const_iterator cbegin() const; + + /** + * Get iterator to end if attributes. + * + * @returns + * Iterator to end of attributes. + */ + const_iterator cend() const; + + private: + /** Collection of attributes. */ + std::vector attributes_; + + /** Size of attributes. */ + std::size_t size_; +}; + +} diff --git a/include/iris/graphics/vertex_data.h b/include/iris/graphics/vertex_data.h new file mode 100644 index 00000000..e071e1bf --- /dev/null +++ b/include/iris/graphics/vertex_data.h @@ -0,0 +1,152 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "core/colour.h" +#include "core/vector3.h" +#include "graphics/bone.h" +#include "graphics/vertex_attributes.h" + +namespace iris +{ + +/** + * Struct encapsulating all data needed to render a vertex. This automatically + * pads fields required by some graphics APIs. + */ +struct VertexData +{ + VertexData() = default; + + /** + * Create a new VertexData. + * + * @param position + * The position of the vertex. + * + * @param normal + * The normal of the vertex. + * + * @param colour + * Colour of the vertex. + * + * @param texture_coords + * Coordinates of texture. + */ + VertexData(const Vector3 &position, const Vector3 &normal, const Colour &colour, const Vector3 &texture_coords) + : VertexData( + position, + normal, + colour, + texture_coords, + {}, + {}, + {{{0u, 1.0f}, {0u, 0.0f}, {0u, 0.0f}, {0u, 0.0f}}}) + { + } + + VertexData( + const Vector3 &position, + const Vector3 &normal, + const Colour &colour, + const Vector3 &texture_coords, + const Vector3 &tangent, + const Vector3 &bitangent) + : VertexData( + position, + normal, + colour, + texture_coords, + tangent, + bitangent, + {{{0u, 1.0f}, {0u, 0.0f}, {0u, 0.0f}, {0u, 0.0f}}}) + { + } + + VertexData( + const Vector3 &position, + const Vector3 &normal, + const Colour &colour, + const Vector3 &texture_coords, + const Vector3 &tangent, + const Vector3 &bitangent, + std::array weights) + : position(position) + , pos_w(1.0f) + , normal(normal) + , normal_w(0.0f) + , colour(colour) + , texture_coords(texture_coords) + , padding(1.0f) + , tangent(tangent) + , tangent_w(0.0f) + , bitangent(bitangent) + , bitangent_w(0.0f) + , bone_ids({}) + , bone_weights({}) + { + for (auto i = 0u; i < weights.size(); ++i) + { + bone_ids[i] = weights[i].vertex; + bone_weights[i] = weights[i].weight; + } + } + + /** Vertex position. */ + Vector3 position; + + /** w component so we can pass position as 4 floats. */ + float pos_w; + + /** Vertex normal. */ + Vector3 normal; + + /** w component so we can pass normal as 4 floats. */ + float normal_w; + + /** Vertex colour. */ + Colour colour; + + /** Texture coordinates. */ + Vector3 texture_coords; + + /** Padding so we can pass normal as 4 floats. */ + float padding; + + /** Normal tangent. */ + Vector3 tangent; + + /** Padding so we can pass normal as 4 floats. */ + float tangent_w; + + /** Normal bitangent tangent. */ + Vector3 bitangent; + + /** Padding so we can pass normal as 4 floats. */ + float bitangent_w; + + /** Array of bone ids. */ + std::array bone_ids; + + /** Array of bone weights. */ + std::array bone_weights; +}; + +/** + * VertexAttributes for above struct. + */ +static VertexAttributes DefaultVertexAttributes{ + {VertexAttributeType::FLOAT_4, + VertexAttributeType::FLOAT_4, + VertexAttributeType::FLOAT_4, + VertexAttributeType::FLOAT_4, + VertexAttributeType::FLOAT_4, + VertexAttributeType::FLOAT_4, + VertexAttributeType::UINT32_4, + VertexAttributeType::FLOAT_4}}; + +} diff --git a/include/iris/graphics/weight.h b/include/iris/graphics/weight.h new file mode 100644 index 00000000..9c143767 --- /dev/null +++ b/include/iris/graphics/weight.h @@ -0,0 +1,42 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** + * A struct encapsulating how much influence a bone has over a vertex (i.e. it's + * weight). + */ +struct Weight +{ + /** + * Construct a new weight. + * + * @param vertex + * Index of vertex this weight applies to. + * + * @param weight + * Influence over vertex, must be in the range [0.0, 1.0]. + */ + Weight(std::uint32_t vertex, float weight) + : vertex(vertex) + , weight(weight) + { + } + + /** Vertex index.*/ + std::uint32_t vertex; + + /** Influence. */ + float weight; +}; + +} diff --git a/include/iris/graphics/win32/win32_d3d12_window.h b/include/iris/graphics/win32/win32_d3d12_window.h new file mode 100644 index 00000000..fc7feac9 --- /dev/null +++ b/include/iris/graphics/win32/win32_d3d12_window.h @@ -0,0 +1,36 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/win32/win32_window.h" + +namespace iris +{ + +/** + * Implementation of Win32Window for d3d12. + */ +class Win32D3D12Window : public Win32Window +{ + public: + /** + * Create a Win32D3D12Window. + * + * @param width + * Width of window. + * + * @param height + * Height of window. + */ + Win32D3D12Window(std::uint32_t width, std::uint32_t height); + + ~Win32D3D12Window() override = default; +}; + +} diff --git a/include/iris/graphics/win32/win32_opengl_window.h b/include/iris/graphics/win32/win32_opengl_window.h new file mode 100644 index 00000000..66ece2ad --- /dev/null +++ b/include/iris/graphics/win32/win32_opengl_window.h @@ -0,0 +1,36 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/win32/win32_window.h" + +namespace iris +{ + +/** + * Implementation of Win32Window for OpenGL. + */ +class Win32OpenGLWindow : public Win32Window +{ + public: + /** + * Create a Win32OpenGLWindow. + * + * @param width + * Width of window. + * + * @param height + * Height of window. + */ + Win32OpenGLWindow(std::uint32_t width, std::uint32_t height); + + ~Win32OpenGLWindow() override = default; +}; + +} diff --git a/include/iris/graphics/win32/win32_window.h b/include/iris/graphics/win32/win32_window.h new file mode 100644 index 00000000..fbde654d --- /dev/null +++ b/include/iris/graphics/win32/win32_window.h @@ -0,0 +1,83 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#define WIN32_LEAN_AND_MEAN +#include + +#include "core/auto_release.h" +#include "events/event.h" +#include "graphics/window.h" + +namespace iris +{ + +/** + * Abstract implementation of Window for win32. This class has concrete + * implementations for each supported graphics api. + */ +class Win32Window : public Window +{ + public: + // helper aliases + using AutoWindow = iris::AutoRelease; + using AutoDC = iris::AutoRelease; + + /** + * Construct a new Win32Window. + * + * @param width + * Width of window. + * + * @param height + * Height of window. + */ + Win32Window(std::uint32_t width, std::uint32_t height); + ~Win32Window() override = default; + + /** + * Get the natural scale for the screen. This value reflects the scale + * factor needed to convert from the default logical coordinate space into + * the device coordinate space of this screen. + * + * @returns + * Screen scale factor. + */ + std::uint32_t screen_scale() const override; + + /** + * Pump the next user input event. Result will be empty if there are no + * new events. + * + * @returns + * Optional event. + */ + std::optional pump_event() override; + + /** + * Get device context handle. + * + * @returns + * Device context handle. + */ + HDC device_context() const; + + protected: + /** Win32 window handle. */ + AutoWindow window_; + + /** Win32 device context handle. */ + AutoDC dc_; + + /** Win32 window class object. */ + WNDCLASSA wc_; +}; + +} diff --git a/include/iris/graphics/win32/win32_window_manager.h b/include/iris/graphics/win32/win32_window_manager.h new file mode 100644 index 00000000..d86281e3 --- /dev/null +++ b/include/iris/graphics/win32/win32_window_manager.h @@ -0,0 +1,50 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "graphics/window.h" +#include "graphics/window_manager.h" + +namespace iris +{ + +/** + * Implementation of WindowManager for win32. + */ +class Win32WindowManager : public WindowManager +{ + public: + ~Win32WindowManager() override = default; + + /** + * Create a new Window. + * + * @param width + * Width of window. + * + * @param height + * Height of window. + */ + Window *create_window(std::uint32_t width, std::uint32_t height) override; + + /** + * Get the currently active window. + * + * @returns + * Pointer to current window, nullptr if one does not exist. + */ + Window *current_window() const override; + + private: + /** Current window .*/ + std::unique_ptr current_window_; +}; + +} diff --git a/include/iris/graphics/window.h b/include/iris/graphics/window.h new file mode 100644 index 00000000..1f0c8381 --- /dev/null +++ b/include/iris/graphics/window.h @@ -0,0 +1,125 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "events/event.h" +#include "graphics/render_pass.h" +#include "graphics/render_target.h" +#include "graphics/renderer.h" + +namespace iris +{ + +/** + * Abstract class for a native window. + */ +class Window +{ + public: + /** + * Create and display a new native window. + * + * @param width + * Width of the window. + * + * @param height + * Height of the window. + */ + Window(std::uint32_t width, std::uint32_t height); + + virtual ~Window() = default; + + /** Disabled */ + Window(const Window &) = delete; + Window &operator=(const Window &) = delete; + + /** + * Pump the next user input event. Result will be empty if there are no + * new events. + * + * @returns + * Optional event. + */ + virtual std::optional pump_event() = 0; + + /** + * Render the current scene. + */ + virtual void render() const; + + /** + * Get the natural scale for the screen. This value reflects the scale + * factor needed to convert from the default logical coordinate space into + * the device coordinate space of this screen. + * + * @returns + * Screen scale factor. + */ + virtual std::uint32_t screen_scale() const = 0; + + /** + * Get the width of the window. + * + * @returns + * Window width. + */ + std::uint32_t width() const; + + /** + * Get the height of the window. + * + * @returns + * Window height. + */ + std::uint32_t height() const; + + /** + * Create a RenderTarget the same size as the Window. + * + * @returns + * RenderTarget. + */ + RenderTarget *create_render_target(); + + /** + * Create a RenderTarget with custom dimensions. + * + * @param width + * Width of render target. + * + * @param height + * Height of render target. + * + * @returns + * RenderTarget. + */ + RenderTarget *create_render_target(std::uint32_t width, std::uint32_t height); + + /** + * Set the render passes. These will be executed when render() is called. + * + * @param render_passes + * Collection of RenderPass objects to render. + */ + void set_render_passes(const std::vector &render_passes); + + protected: + /** Window width. */ + std::uint32_t width_; + + /** Window height. */ + std::uint32_t height_; + + /** Renderer to use. */ + std::unique_ptr renderer_; +}; + +} diff --git a/include/iris/graphics/window_manager.h b/include/iris/graphics/window_manager.h new file mode 100644 index 00000000..33dcb782 --- /dev/null +++ b/include/iris/graphics/window_manager.h @@ -0,0 +1,44 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +class Window; + +/** + * Interface for a class which creates Window objects. + */ +class WindowManager +{ + public: + virtual ~WindowManager() = default; + + /** + * Create a new Window. + * + * @param width + * Width of window. + * + * @param height + * Height of window. + */ + virtual Window *create_window(std::uint32_t width, std::uint32_t height) = 0; + + /** + * Get the currently active window. + * + * @returns + * Pointer to current window, nullptr if one does not exist. + */ + virtual Window *current_window() const = 0; +}; + +} diff --git a/include/iris/iris_version.h.in b/include/iris/iris_version.h.in new file mode 100644 index 00000000..9d6aeb81 --- /dev/null +++ b/include/iris/iris_version.h.in @@ -0,0 +1,10 @@ +#pragma once + +#define IRIS_VERSION_MAJOR @iris_VERSION_MAJOR@ +#define IRIS_VERSION_MINOR @iris_VERSION_MINOR@ +#define IRIS_VERSION_PATCH @iris_VERSION_PATCH@ +#define IRIS_VERSION_MAJOR_STR "@iris_VERSION_MAJOR@" +#define IRIS_VERSION_MINOR_STR "@iris_VERSION_MINOR@" +#define IRIS_VERSION_PATCH_STR "@iris_VERSION_PATCH@" +#define IRIS_VERSION_STR IRIS_VERSION_MAJOR_STR "." IRIS_VERSION_MINOR_STR "." IRIS_VERSION_PATCH_STR + diff --git a/include/iris/jobs/arch/x86_64/context.h b/include/iris/jobs/arch/x86_64/context.h new file mode 100644 index 00000000..f87b3f7a --- /dev/null +++ b/include/iris/jobs/arch/x86_64/context.h @@ -0,0 +1,28 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +/** + * This is an incomplete file and is intended to be included in + * include/jobs/context.h + * + * *DO NOT* include this file directly + */ + +/** + * x86_64 context, registers that need to be preserved when suspending a + * function. + */ +struct Context +{ + void *rbx = nullptr; + void *rbp = nullptr; + void *rsp = nullptr; + void *rip = nullptr; + void *r12 = nullptr; + void *r13 = nullptr; + void *r14 = nullptr; + void *r15 = nullptr; +}; diff --git a/include/iris/jobs/arch/x86_64/functions.S b/include/iris/jobs/arch/x86_64/functions.S new file mode 100644 index 00000000..ea01ac60 --- /dev/null +++ b/include/iris/jobs/arch/x86_64/functions.S @@ -0,0 +1,90 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +.intel_syntax noprefix + +.section __TEXT,__text + +/** + * Change stack, performs all necessary patching such that the function can + * return to the caller. + * + * @param rdi + * Pointer to new stack. + */ +.globl _change_stack +_change_stack: + +push rax # stack alignment + +mov r9, rbp +mov r8, [rbp] # get base-pointer for previous frame +sub r8, rbp # get offset between this and previous frame + +mov rdx, rbp +sub rdx, rsp # get size of current frame +mov rax, rdi # preserve new stack address + +# copy old stack to new stack +cld +mov rsi, rsp +mov rcx, 1024 +rep movsb + +mov rsp, rax # switch to new stack +mov rbp, rax # copy stack to base pointer +add rbp, rdx # restore stack frame by offseting base pointer the same amout as + # when we entered the function + +# patch base pointer for previous frame to point back into our new stack, this +# allows us one function return after this function +mov rax, r8 +add r8, rbp +mov [rbp], r8 + +pop rax # stack alignment + +ret + +/** + * Save current context. + * + * @param context + * Pointer to struct to save registers. + */ +.globl _save_context +_save_context: +mov [rdi], rbx +mov [rdi + 8], rbp +mov [rdi + 16], rsp +lea rax, [rip+end] # store end of this function in rip +mov [rdi + 24], rax +mov [rdi + 32], r12 +mov [rdi + 40], r13 +mov [rdi + 48], r14 +mov [rdi + 56], r15 +end: +ret + +/** + * Restore context. + * + * @param context + * Pointer to struct with registers to restore. + */ +.globl _restore_context +_restore_context: +mov rbx, [rdi] +mov rbp, [rdi + 8] +mov rsp, [rdi + 16] +mov r12, [rdi + 32] +mov r13, [rdi + 40] +mov r14, [rdi + 48] +mov r15, [rdi + 56] +mov rax, [rdi + 24] +jmp rax + + diff --git a/include/iris/jobs/concurrent_queue.h b/include/iris/jobs/concurrent_queue.h new file mode 100644 index 00000000..addcabb6 --- /dev/null +++ b/include/iris/jobs/concurrent_queue.h @@ -0,0 +1,125 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "core/exception.h" + +namespace iris +{ + +/** + * A container adaptor for a thread-safe FIFO queue. + */ +template > +class ConcurrentQueue +{ + public: + // member types + using container_type = Container; + using size_type = typename Container::size_type; + using value_type = typename Container::value_type; + using reference = typename Container::reference; + + /** + * Construct an empty queue. + */ + ConcurrentQueue() + : container_() + , mutex_() + , empty_(true) + { + } + + /** + * Check if the queue is empty. + * + * @returns + * True if queue is empty, else false. + */ + bool empty() const + { + return empty_; + } + + /** + * Add an item to the end of the queue. + * + * @param args + * Arguments for object being places in queue, will be + * perfectly forwarded. + */ + template + void enqueue(Args &&...args) + { + std::unique_lock lock(mutex_); + + container_.emplace_back(std::forward(args)...); + + empty_ = false; + } + + /** + * Tries to pop the next element off the queue. + * + * @param element + * Reference to store popped element. + * + * @returns + * True if an element could be dequeued, false otherwise. + */ + bool try_dequeue(reference element) + { + auto dequeued = false; + + std::unique_lock lock(mutex_, std::try_to_lock); + + if (!empty_ && lock.owns_lock()) + { + element = std::move(container_.front()); + container_.pop_front(); + + empty_ = container_.empty(); + dequeued = true; + } + + return dequeued; + } + + /** + * Pops the next element off the queue. Blocks until it can perform + * operation. + * + * @returns + * Popped element. + */ + value_type dequeue() + { + std::unique_lock lock(mutex_); + + value_type value(std::move(container_.front())); + container_.pop_front(); + + empty_ = container_.empty(); + + return value; + } + + private: + /** Queue container. */ + container_type container_; + + /** Mutex for queue. */ + std::mutex mutex_; + + /** Flag indicating whether queue is empty. */ + std::atomic empty_; +}; + +} diff --git a/include/iris/jobs/context.h b/include/iris/jobs/context.h new file mode 100644 index 00000000..d75ea8db --- /dev/null +++ b/include/iris/jobs/context.h @@ -0,0 +1,20 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace iris +{ + +// include arch specific files + +#if defined(IRIS_ARCH_X86_64) +#include "jobs/arch/x86_64/context.h" +#else +#error unsupported architecture +#endif + +} diff --git a/include/iris/jobs/fiber/counter.h b/include/iris/jobs/fiber/counter.h new file mode 100644 index 00000000..e0ee5f94 --- /dev/null +++ b/include/iris/jobs/fiber/counter.h @@ -0,0 +1,61 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +namespace iris +{ + +/** + * A thread-safe counter. Can be decremented and checked. + */ +class Counter +{ + public: + /** + * Construct counter with initial value. + * + * @param value + * Initial value of counter. + */ + explicit Counter(int value); + + // disable copy and move + Counter(const Counter &) = delete; + Counter &operator=(const Counter &) = delete; + Counter(Counter &&) = delete; + Counter &operator=(Counter &&) = delete; + + /** + * Cast counter value to int. + * + * @returns + * Value of counter. + */ + operator int(); + + /** + * Prefix decrement counter. + */ + void operator--(); + + /** + * Postfix decrement counter. + */ + void operator--(int); + + private: + /** Value of counter. */ + int value_; + + /** Lock for object. */ + std::mutex mutex_; +}; + +} diff --git a/include/iris/jobs/fiber/fiber.h b/include/iris/jobs/fiber/fiber.h new file mode 100644 index 00000000..fdfab6ab --- /dev/null +++ b/include/iris/jobs/fiber/fiber.h @@ -0,0 +1,142 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "core/static_buffer.h" +#include "jobs/fiber/counter.h" +#include "jobs/job.h" + +namespace iris +{ + +/* + * A Fiber is a user-land thread. It maintains it's own stack and can be + * suspended and resumed (cooperative multi-threading). A Fiber will be started + * from a thread but can be suspended and resumed from a different one. In + * general one should not have to manually create Fibers, they are an internal + * class. For parallelising work the JobSystem should be used. + */ +class Fiber +{ + public: + /** + * Construct a Fiber with a job to run. + * + * @param job + * Job to run. + */ + explicit Fiber(Job job); + + /** + * Construct a Fiber with a job and a counter. + * + * @param job + * Job to run. + * + * @param counter + * Counter to decrement when job is done. + */ + Fiber(Job job, Counter *counter); + + ~Fiber(); + + Fiber(const Fiber &) = delete; + Fiber &operator=(const Fiber &) = delete; + Fiber(Fiber &&other) = delete; + Fiber &operator=(Fiber &&other) = delete; + + /** + * Start the fiber. + */ + void start(); + + /** + * Suspends a Fibers execution, execution will continue from where + * start was called. + */ + void suspend(); + + /** + * Resume a suspended Fiber. Execution will continue from where suspend + * was called. + * + * It is undefined behavior to resume a non-suspended Fiber. + */ + void resume(); + + /** + * Check if a Fiber is safe to call methods on. It is only not safe when it + * has been suspended but not yet restored its original context. + * + * @returns + * True if this Fiber is safe false otherwise. + */ + bool is_safe() const; + + /** + * Set fiber to be unsafe. + */ + void set_unsafe(); + + /** + * Check if another fiber is waiting for this to finish. + * + * @returns + * True if another fiber is waiting on this, otherwise false. + */ + bool is_being_waited_on() const; + + /** + * Get any exception thrown during the execution of this fiber. + * + * @returns + * exception_ptr to throw exception, nullptr of none were thrown. + */ + std::exception_ptr exception() const; + + /** + * Convert current thread to fiber. This must be called once (and only once) + * on each thread that wants to execute fibers. + */ + static void thread_to_fiber(); + + /** + * Gets a pointer to the current fiber running on the calling thread. + * If no Fiber is running then nullptr. + * + * @returns + * Pointer to pointer to running Fiber, or nullptr if no Fiber is + * running. + */ + static Fiber **this_fiber(); + + private: + /** Job to run in Fiber. */ + Job job_; + + /** optional counter. */ + Counter *counter_; + + /** Optional parent fiber. */ + Fiber *parent_fiber_; + + /** Pointer storing job exception. */ + std::exception_ptr exception_; + + /** Flag if fiber is not safe to operator on. */ + std::atomic safe_; + + /** Pointer to implementation. */ + struct implementation; + std::unique_ptr impl_; +}; + +} diff --git a/include/iris/jobs/fiber/fiber_job_system.h b/include/iris/jobs/fiber/fiber_job_system.h new file mode 100644 index 00000000..e4771282 --- /dev/null +++ b/include/iris/jobs/fiber/fiber_job_system.h @@ -0,0 +1,66 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "core/semaphore.h" +#include "core/thread.h" +#include "jobs/concurrent_queue.h" +#include "jobs/fiber/counter.h" +#include "jobs/fiber/fiber.h" +#include "jobs/job.h" +#include "jobs/job_system.h" + +namespace iris +{ + +/** + * Implementation of JobSystem that schedules its jobs using fibers. + */ +class FiberJobSystem : public JobSystem +{ + public: + FiberJobSystem(); + ~FiberJobSystem() override; + + /** + * Add a collection of jobs. Once added these are executed in a + * fire-and-forget manner, there is no way to wait on them to finish or + * to know when they have executed. + * + * @param jobs + * Jobs to execute. + */ + void add_jobs(const std::vector &jobs) override; + + /** + * Add a collection of jobs. Once added this call blocks until all + * jobs have finished executing. + * + * @param jobs + * Jobs to execute. + */ + void wait_for_jobs(const std::vector &jobs) override; + + private: + /** Flag indicating of system is running. */ + std::atomic running_; + + /** Semaphore signally how many fibers are available. */ + Semaphore jobs_semaphore_; + + /** Worker threads which execute fibers. */ + std::vector workers_; + + /** Queue of fibers.*/ + ConcurrentQueue> fibers_; +}; + +} diff --git a/include/iris/jobs/fiber/fiber_job_system_manager.h b/include/iris/jobs/fiber/fiber_job_system_manager.h new file mode 100644 index 00000000..e7a13501 --- /dev/null +++ b/include/iris/jobs/fiber/fiber_job_system_manager.h @@ -0,0 +1,59 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "jobs/fiber/fiber_job_system.h" +#include "jobs/job.h" +#include "jobs/job_system_manager.h" + +namespace iris +{ + +/** + * Implementation of JobSystemManager for FiberJobSystem. + */ +class FiberJobSystemManager : public JobSystemManager +{ + public: + ~FiberJobSystemManager() override = default; + + /** + * Create a JobSystem. + * + * @returns + * Pointer to JobSystem. + */ + JobSystem *create_job_system() override; + + /** + * Add a collection of jobs. Once added these are executed in a + * fire-and-forget manner, there is no way to wait on them to finish or + * to know when they have executed. + * + * @param jobs + * Jobs to execute. + */ + void add(const std::vector &jobs) override; + + /** + * Add a collection of jobs. Once added this call blocks until all + * jobs have finished executing. + * + * @param jobs + * Jobs to execute. + */ + void wait(const std::vector &jobs) override; + + private: + /** Current JobSystem. */ + std::unique_ptr job_system_; +}; + +} diff --git a/include/iris/jobs/job.h b/include/iris/jobs/job.h new file mode 100644 index 00000000..76e37e21 --- /dev/null +++ b/include/iris/jobs/job.h @@ -0,0 +1,19 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +namespace iris +{ + +// convenient alias for a job +using Job = std::function; + +} diff --git a/include/iris/jobs/job_system.h b/include/iris/jobs/job_system.h new file mode 100644 index 00000000..1aee76e4 --- /dev/null +++ b/include/iris/jobs/job_system.h @@ -0,0 +1,50 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "jobs/job.h" + +namespace iris +{ + +/** + * Interface for a class managing the scheduling and running of jobs. + */ +class JobSystem +{ + public: + JobSystem() = default; + virtual ~JobSystem() = default; + + JobSystem(const JobSystem &) = delete; + JobSystem &operator=(const JobSystem &) = delete; + JobSystem(JobSystem &&) = delete; + JobSystem &operator=(JobSystem &&) = delete; + + /** + * Add a collection of jobs. Once added these are executed in a + * fire-and-forget manner, there is no way to wait on them to finish or + * to know when they have executed. + * + * @param jobs + * Jobs to execute. + */ + virtual void add_jobs(const std::vector &jobs) = 0; + + /** + * Add a collection of jobs. Once added this call blocks until all + * jobs have finished executing. + * + * @param jobs + * Jobs to execute. + */ + virtual void wait_for_jobs(const std::vector &jobs) = 0; +}; + +} diff --git a/include/iris/jobs/job_system_manager.h b/include/iris/jobs/job_system_manager.h new file mode 100644 index 00000000..2a1c9e97 --- /dev/null +++ b/include/iris/jobs/job_system_manager.h @@ -0,0 +1,54 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "jobs/job.h" +#include "jobs/job_system.h" + +namespace iris +{ + +/** + * Interface for a class that manages JobSystem objects. This used as part of + * component registration in the Root. + */ +class JobSystemManager +{ + public: + virtual ~JobSystemManager() = default; + + /** + * Create a JobSystem. + * + * @returns + * Pointer to JobSystem. + */ + virtual JobSystem *create_job_system() = 0; + + /** + * Add a collection of jobs. Once added these are executed in a + * fire-and-forget manner, there is no way to wait on them to finish or + * to know when they have executed. + * + * @param jobs + * Jobs to execute. + */ + virtual void add(const std::vector &jobs) = 0; + + /** + * Add a collection of jobs. Once added this call blocks until all + * jobs have finished executing. + * + * @param jobs + * Jobs to execute. + */ + virtual void wait(const std::vector &jobs) = 0; +}; + +} diff --git a/include/iris/jobs/thread/thread_job_system.h b/include/iris/jobs/thread/thread_job_system.h new file mode 100644 index 00000000..8acb7b07 --- /dev/null +++ b/include/iris/jobs/thread/thread_job_system.h @@ -0,0 +1,52 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "jobs/job.h" +#include "jobs/job_system.h" + +namespace iris +{ + +/** + * Implementation of JobSystem that schedules its jobs using threads. + */ +class ThreadJobSystem : public JobSystem +{ + public: + ThreadJobSystem(); + ~ThreadJobSystem() override = default; + + /** + * Add a collection of jobs. Once added these are executed in a + * fire-and-forget manner, there is no way to wait on them to finish or + * to know when they have executed. + * + * @param jobs + * Jobs to execute. + */ + void add_jobs(const std::vector &jobs) override; + + /** + * Add a collection of jobs. Once added this call blocks until all + * jobs have finished executing. + * + * @param jobs + * Jobs to execute. + */ + void wait_for_jobs(const std::vector &jobs) override; + + private: + /** Flag indicating of system is running. */ + std::atomic running_; +}; + +} diff --git a/include/iris/jobs/thread/thread_job_system_manager.h b/include/iris/jobs/thread/thread_job_system_manager.h new file mode 100644 index 00000000..93f3c368 --- /dev/null +++ b/include/iris/jobs/thread/thread_job_system_manager.h @@ -0,0 +1,59 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "jobs/job.h" +#include "jobs/job_system_manager.h" +#include "jobs/thread/thread_job_system.h" + +namespace iris +{ + +/** + * Implementation of JobSystemManager for ThreadJobSystem. + */ +class ThreadJobSystemManager : public JobSystemManager +{ + public: + ~ThreadJobSystemManager() override = default; + + /** + * Create a JobSystem. + * + * @returns + * Pointer to JobSystem. + */ + JobSystem *create_job_system() override; + + /** + * Add a collection of jobs. Once added these are executed in a + * fire-and-forget manner, there is no way to wait on them to finish or + * to know when they have executed. + * + * @param jobs + * Jobs to execute. + */ + void add(const std::vector &jobs) override; + + /** + * Add a collection of jobs. Once added this call blocks until all + * jobs have finished executing. + * + * @param jobs + * Jobs to execute. + */ + void wait(const std::vector &jobs) override; + + private: + /** Current JobSystem. */ + std::unique_ptr job_system_; +}; + +} diff --git a/include/iris/log/basic_formatter.h b/include/iris/log/basic_formatter.h new file mode 100644 index 00000000..e2f6f751 --- /dev/null +++ b/include/iris/log/basic_formatter.h @@ -0,0 +1,64 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "log/formatter.h" +#include "log/log_level.h" + +namespace iris +{ + +/** + * Implementation of Formatter which formats log message details as: + * L T [t] F:L | M + * + * Where: + * L : First letter of log level + * T : Time stamp + * t : Tag + * F : Filename + * L : Line number + * M : message + * + * Example: + * D 17:38:28 [camera] camera.cpp:58 | x: -4.37114e-08 y: 0 z: -1 + */ +class BasicFormatter : public Formatter +{ + public: + /** Default */ + ~BasicFormatter() override = default; + + /** + * Format the supplied log details into a string. + * + * @param level + * Log level. + * + * @param tag + * Tag for log message. + * + * @param message + * Log message. + * + * @param filename + * Name of the file logging the message. + * + * @param line + * Line of the log call in the file. + */ + std::string format( + const LogLevel level, + const std::string &tag, + const std::string &message, + const std::string &filename, + const int line) override; +}; + +} diff --git a/include/iris/log/colour_formatter.h b/include/iris/log/colour_formatter.h new file mode 100644 index 00000000..656ac8d2 --- /dev/null +++ b/include/iris/log/colour_formatter.h @@ -0,0 +1,57 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "log/basic_formatter.h" +#include "log/formatter.h" +#include "log/log_level.h" + +namespace iris +{ + +/** + * Implementation of Formatter which applies colour to basic formatter. + */ +class ColourFormatter : public Formatter +{ + public: + /** Default */ + ~ColourFormatter() override = default; + + /** + * Format the supplied log details into a string. + * + * @param level + * Log level. + * + * @param tag + * Tag for log message. + * + * @param message + * Log message. + * + * @param filename + * Name of the file logging the message. + * + * @param line + * Line of the log call in the file. + */ + std::string format( + const LogLevel level, + const std::string &tag, + const std::string &message, + const std::string &filename, + const int line) override; + + private: + /** Use BasicFormatter for formatting. */ + BasicFormatter formatter_; +}; + +} diff --git a/include/iris/log/emoji_formatter.h b/include/iris/log/emoji_formatter.h new file mode 100644 index 00000000..4349ad8a --- /dev/null +++ b/include/iris/log/emoji_formatter.h @@ -0,0 +1,58 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "log/basic_formatter.h" +#include "log/formatter.h" +#include "log/log_level.h" + +namespace iris +{ + +/** + * Implementation of Formatter which prepends messages with emojis. Useful for + * consoles which do not support colour (e.g. Xcode). + */ +class EmojiFormatter : public Formatter +{ + public: + /** Default */ + ~EmojiFormatter() override = default; + + /** + * Format the supplied log details into a string. + * + * @param level + * Log level. + * + * @param tag + * Tag for log message. + * + * @param message + * Log message. + * + * @param filename + * Name of the file logging the message. + * + * @param line + * Line of the log call in the file. + */ + std::string format( + const LogLevel level, + const std::string &tag, + const std::string &message, + const std::string &filename, + const int line) override; + + private: + /** Use BasicFormatter for formatting. */ + BasicFormatter formatter_; +}; + +} diff --git a/include/iris/log/file_outputter.h b/include/iris/log/file_outputter.h new file mode 100644 index 00000000..9075f1ca --- /dev/null +++ b/include/iris/log/file_outputter.h @@ -0,0 +1,47 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "log/outputter.h" + +namespace iris +{ + +/** + * Implementation of Outputter which writes log messages to filet. + */ +class FileOutputter : public Outputter +{ + public: + /** Default */ + ~FileOutputter() override = default; + + /** + * Construct a new file_outputter. + * + * @param filename + * Name of log file to write to. + */ + explicit FileOutputter(const std::string &filename); + + /** + * Output log. + * + * @param log + * Log message to output. + */ + void output(const std::string &log) override; + + private: + /** File stream to write to. */ + std::ofstream file_; +}; + +} diff --git a/include/iris/log/formatter.h b/include/iris/log/formatter.h new file mode 100644 index 00000000..d1032af7 --- /dev/null +++ b/include/iris/log/formatter.h @@ -0,0 +1,52 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "log/log_level.h" + +namespace iris +{ + +/** + * Interface for a formatter, a class which takes log message details and + * formats them into a string. + */ +class Formatter +{ + public: + /** Default */ + virtual ~Formatter() = default; + + /** + * Format the supplied log details into a string. + * + * @param level + * Log level. + * + * @param tag + * Tag for log message. + * + * @param message + * Log message. + * + * @param filename + * Name of the file logging the message. + * + * @param line + * Line of the log call in the file. + */ + virtual std::string format( + const LogLevel level, + const std::string &tag, + const std::string &message, + const std::string &filename, + const int line) = 0; +}; + +} diff --git a/include/iris/log/log.h b/include/iris/log/log.h new file mode 100644 index 00000000..5ee75504 --- /dev/null +++ b/include/iris/log/log.h @@ -0,0 +1,44 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "log/log_level.h" +#include "log/logger.h" + +#if !defined(NDEBUG) + +// convenient macros for logging +#define LOG_DEBUG(T, ...) iris::Logger::instance().log(iris::LogLevel::DEBUG, T, __FILE__, __LINE__, false, __VA_ARGS__) +#define LOG_INFO(T, ...) iris::Logger::instance().log(iris::LogLevel::INFO, T, __FILE__, __LINE__, false, __VA_ARGS__) +#define LOG_WARN(T, ...) iris::Logger::instance().log(iris::LogLevel::WARN, T, __FILE__, __LINE__, false, __VA_ARGS__) +#define LOG_ERROR(T, ...) iris::Logger::instance().log(iris::LogLevel::ERR, T, __FILE__, __LINE__, false, __VA_ARGS__) + +// convenient macros for engine logging +#define LOG_ENGINE_DEBUG(T, ...) \ + iris::Logger::instance().log(iris::LogLevel::DEBUG, T, __FILE__, __LINE__, true, __VA_ARGS__) +#define LOG_ENGINE_INFO(T, ...) \ + iris::Logger::instance().log(iris::LogLevel::INFO, T, __FILE__, __LINE__, true, __VA_ARGS__) +#define LOG_ENGINE_WARN(T, ...) \ + iris::Logger::instance().log(iris::LogLevel::WARN, T, __FILE__, __LINE__, true, __VA_ARGS__) +#define LOG_ENGINE_ERROR(T, ...) \ + iris::Logger::instance().log(iris::LogLevel::ERR, T, __FILE__, __LINE__, true, __VA_ARGS__) + +#else + +// convenient macros for logging +#define LOG_DEBUG(T, ...) +#define LOG_INFO(T, ...) +#define LOG_WARN(T, ...) +#define LOG_ERROR(T, ...) + +// convenient macros for engine logging +#define LOG_ENGINE_DEBUG(T, ...) +#define LOG_ENGINE_INFO(T, ...) +#define LOG_ENGINE_WARN(T, ...) +#define LOG_ENGINE_ERROR(T, ...) + +#endif diff --git a/include/iris/log/log_level.h b/include/iris/log/log_level.h new file mode 100644 index 00000000..455f6759 --- /dev/null +++ b/include/iris/log/log_level.h @@ -0,0 +1,54 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +namespace iris +{ + +/** + * Enumeration of log levels. + */ +enum class LogLevel : std::uint32_t +{ + DEBUG, + INFO, + WARN, + ERR +}; + +/** + * Helper function to write a string representation of a LogLevel enum to a + * stream. + * + * @param out + * Stream to write to. + * + * @param level + * Log level to write. + * + * @returns + * Reference to input stream. + */ +inline std::ostream &operator<<(std::ostream &out, const LogLevel level) +{ + switch (level) + { + case LogLevel::DEBUG: out << "DEBUG"; break; + case LogLevel::INFO: out << "INFO"; break; + case LogLevel::WARN: out << "WARN"; break; + case LogLevel::ERR: out << "ERROR"; break; + default: out << "UNKNOWN"; break; + } + + return out; +} + +} diff --git a/include/iris/log/logger.h b/include/iris/log/logger.h new file mode 100644 index 00000000..0aa41241 --- /dev/null +++ b/include/iris/log/logger.h @@ -0,0 +1,366 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "log/colour_formatter.h" +#include "log/log_level.h" +#include "log/stdout_outputter.h" + +namespace iris +{ + +namespace detail +{ + +/** + * Helper function to write a string to a stream. Writes from the supplied + * position to the first occurrence of '{}' (which is replaced by supplied + * object). If '{}' is not found then nothing is written. + * + * @param format_str + * The format string to write. + * + * @param obj + * The object to write. + * + * @param pos + * [in/out] On function enter this is the position to start searching in + * format_str for '{}'. On exit in the position in format_str after the '{}'. + * If no '{}' is found in the string then it is std::string::npos. + * + * @param strm + * The stream to write to. + */ +template +void format(const std::string &format_str, T &&obj, std::size_t &pos, std::stringstream &strm) +{ + static const std::string format_pattern{"{}"}; + + const auto current_pos = pos; + + // find the pattern + pos = format_str.find(format_pattern, pos); + if (pos != std::string::npos) + { + // write the string up to the pattern then the supplied object + strm << format_str.substr(current_pos, pos - current_pos) << obj; + + // move position passed the format + pos += format_pattern.length(); + } +} + +/** + * Base case for variadic template unpacking. + * + * @param message + * Format string message. + * + * @param pos + * [in/out] On function enter this is the position to start searching in + * format_str for '{}'. On exit in the position in format_str after the '{}'. + * If no '{}' is found in the string then it is std::string::npos. + * + * @param strm + * The stream to write to. + * + * @param head + * The object to write. + */ +template +void unpack(const std::string &message, std::size_t &pos, std::stringstream &strm, Head &&head) +{ + // write object to stream (if '{}' is in message) + format(message, head, pos, strm); + + // we have no more args to write to stream, so write the remainder of the + // message to the string (if there is any left) + if (pos != std::string::npos) + { + strm << message.substr(pos); + } +} + +/** + * Variadic template argument unpacker. + * + * @param message + * Format string message. + * + * @param pos + * [in/out] On function enter this is the position to start searching in + * format_str for '{}'. On exit in the position in format_str after the '{}'. + * If no '{}' is found in the string then it is std::string::npos. + * + * @param strm + * The stream to write to. + * + * @param head + * Current object to write. + * + * @param args + * Remaining arguments + */ +template +void unpack(const std::string &message, std::size_t &pos, std::stringstream &strm, Head &&head, Tail &&...tail) +{ + format(message, std::forward(head), pos, strm); + unpack(message, pos, strm, std::forward(tail)...); +} + +} + +/** + * Singleton class for logging. Formatting and outputting are controlled via + * settable classes, by default uses colour formatting and outputs to stdout. + * + * The supported log levels are: + * DEBUG, + * INFO, + * WARN, + * ERR + * + * It us up to you what to use each for however it is suggested that INFO be + * used for the majority of logging, with WARN and ERR for warnings and errors + * respectively (feel free to decide what constitutes as a warning and error). + * Debug should be used for the log messages you use to diagnose a bug and will + * probably later delete. + */ +class Logger +{ + public: + /** + * Get single instance of Logger. + * + * @returns + * Logger single instance. + */ + static Logger &instance() + { + static Logger logger{}; + return logger; + } + + Logger(const Logger &) = delete; + Logger &operator=(const Logger &) = delete; + Logger(Logger &&) = delete; + Logger &operator=(Logger &&) = delete; + + /** + * Add a tag to be ignored, this prevents any log messages from the + * given tag being processed. + * + * Adding the same tag more than once is a no-op. + * + * @param tag + * Tag to ignore. + */ + void ignore_tag(const std::string &tag) + { + ignore_.emplace(tag); + } + + /** + * Show a supplied tag. This ensures that log messages from the given + * tag are processed. + * + * Adding the same tag more than once or showing a non-hidden tag is a + * no-op. + * + * @param tag + * Tag to show. + */ + void show_tag(const std::string &tag) + { + if (const auto find = ignore_.find(tag); find != std::cend(ignore_)) + { + ignore_.erase(find); + } + } + + /** + * Set minimum log level, anything above this level is not processed. + * + * See log_level.h for definition of log_level. + * + * @param min_level + * Minimum level to process. + */ + void set_min_level(const LogLevel min_level) + { + min_level_ = min_level; + } + + /** + * Set whether internal engine messages should be processed. + * + * @param log_engine + * True if engine messages should be processed, false of not. + */ + void set_log_engine(const bool log_engine) + { + log_engine_ = log_engine; + } + + /** + * Set the Formatter class. + * + * Uses perfect forwarding to construct object. + * + * @param args + * Varaidic list of arguments for Formatter constructor. + */ + template ::value>> + void set_Formatter(Args &&...args) + { + formatter_ = std::make_unique(std::forward(args)...); + } + + /** + * Set the Outputter class. + * + * Uses perfect forwarding to construct object. + * + * @param args + * Varaidic list of arguments for Outputter constructor. + */ + template ::value>> + void set_Outputter(Args &&...args) + { + outputter_ = std::make_unique(std::forward(args)...); + } + + /** + * Log a message. This function handles the case where no arguments + * are supplied i.e. just a log message. + * + * @param level + * Log level. + * + * @param tag + * Tag for log message. + * + * @param filename + * Name of the file logging the message. + * + * @param line + * Line of the log call in the file. + * + * @param engine + * True if this log message is from the internal engine, false + * otherwise. + * + * @param message + * Log message. + */ + void log( + const LogLevel level, + const std::string &tag, + const std::string &filename, + const int line, + const bool engine, + const std::string &message) + { + // check if we want to process this log message + if ((!engine || log_engine_) && (level >= min_level_) && (ignore_.find(tag) == std::cend(ignore_))) + { + std::stringstream strm{}; + strm << message; + + const auto log = formatter_->format(level, tag, strm.str(), filename, line); + + std::unique_lock lock(mutex_); + outputter_->output(log); + } + } + + /** + * Log a message. This function handles the case where there are + * arguments. + * + * @param level + * Log level. + * + * @param tag + * Tag for log message. + * + * @param filename + * Name of the file logging the message. + * + * @param line + * Line of the log call in the file. + * + * @param engine + * True if this log message is from the internal engine, false + * otherwise. + * + * @param message + * Log message. + * + * @param args + * Variadic list of arguments for log formatting. + */ + template + void log( + const LogLevel level, + const std::string &tag, + const std::string &filename, + const int line, + const bool engine, + const std::string &message, + Args &&...args) + { + std::stringstream strm{}; + + // apply string formatting + std::size_t pos = 0u; + detail::unpack(message, pos, strm, std::forward(args)...); + + log(level, tag, filename, line, engine, strm.str()); + } + + private: + /** + * Construct a new logger. + */ + Logger() + : formatter_(std::make_unique()) + , outputter_(std::make_unique()) + , ignore_() + , min_level_(LogLevel::DEBUG) + , log_engine_(false) + , mutex_(){}; + + /** Formatter object. */ + std::unique_ptr formatter_; + + /** Outputter object. */ + std::unique_ptr outputter_; + + /** Collection of tags to ignore. */ + std::set ignore_; + + /** Minimum log level. */ + LogLevel min_level_; + + /** Whether to log internal engine messages. */ + bool log_engine_; + + /** Lock for logging. */ + std::mutex mutex_; +}; + +} diff --git a/include/iris/log/outputter.h b/include/iris/log/outputter.h new file mode 100644 index 00000000..215f979e --- /dev/null +++ b/include/iris/log/outputter.h @@ -0,0 +1,33 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** + * Interface for an outputter, a class which writes a log message to some + * implementation defined medium. + */ +class Outputter +{ + public: + /** Default */ + virtual ~Outputter() = default; + + /** + * Output log. + * + * @param log + * Log message to output. + */ + virtual void output(const std::string &log) = 0; +}; + +} diff --git a/include/iris/log/stdout_outputter.h b/include/iris/log/stdout_outputter.h new file mode 100644 index 00000000..423a97e7 --- /dev/null +++ b/include/iris/log/stdout_outputter.h @@ -0,0 +1,34 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "log/outputter.h" + +namespace iris +{ + +/** + * Implementation of Outputter which writes log messages to stdout. + */ +class StdoutFormatter : public Outputter +{ + public: + /** Default */ + ~StdoutFormatter() override = default; + + /** + * Output log. + * + * @param log + * Log message to output. + */ + void output(const std::string &log) override; +}; + +} diff --git a/include/iris/networking/channel/channel.h b/include/iris/networking/channel/channel.h new file mode 100644 index 00000000..080517ac --- /dev/null +++ b/include/iris/networking/channel/channel.h @@ -0,0 +1,90 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "networking/packet.h" +#include +#include + +namespace iris +{ + +/** + * Abstract class for a channel, a class which enforces guarantees on unreliable + * packet transfer. This class provides an interface for consuming incoming + * and outgoing packets, which when yielded will be in order according to + * the channel guarantees. + * + * For example a channel which provides reliable and ordered guarantee will only + * yield packets that are in order, it will buffer out of order packets until + * the gaps have also been received. It will also handle acks so that dropped + * packets will be resent. + * + * This is built on top of the Packet primitive. + * + * Note that this interface is not responsible for the actual sending and + * receiving of packets. Rather it is a buffer which all packets being sent + * and received should be passed through, anything yielded can then be sent + * over a socket or passed up the application. + * + * local channel remote channel + * +----------------+ +----------------+ + * --->|===== send =====|Y~~~~~~~~~~~~~~~|=== received ===|Y--> + * | | | | + * <--Y|=== received ===|~~~~~~~~~~~~~~~Y|===== send =====|<--- + * +----------------+ +----------------+ + * + * Y - yield + */ +class Channel +{ + public: + Channel() = default; + virtual ~Channel() = default; + + /** + * Enqueue a packet to be sent. + * + * @param packet + * Packet to be sent. + */ + virtual void enqueue_send(Packet packet) = 0; + + /** + * Enqueue a received packet. + * + * @param packet + * Packet received. + */ + virtual void enqueue_receive(Packet packet) = 0; + + /** + * Yield all packets to be sent, according to the channel guarantees. + * + * @returns + * Packets to be send. + */ + virtual std::vector yield_send_queue(); + + /** + * Yield all packets that have been received, according to the channel + * guarantees. + * + * @returns + * Packets received. + */ + virtual std::vector yield_receive_queue(); + + protected: + /** Queue for send packets. */ + std::vector send_queue_; + + /** Queue for received packets. */ + std::vector receive_queue_; +}; + +} diff --git a/include/iris/networking/channel/channel_type.h b/include/iris/networking/channel/channel_type.h new file mode 100644 index 00000000..fec021a4 --- /dev/null +++ b/include/iris/networking/channel/channel_type.h @@ -0,0 +1,25 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** + * Enumeration of channel types. + */ +enum class ChannelType : std::uint8_t +{ + INVAlID, + UNRELIABLE_UNORDERED, + UNRELIABLE_SEQUENCED, + RELIABLE_ORDERED, +}; + +} diff --git a/include/iris/networking/channel/reliable_ordered_channel.h b/include/iris/networking/channel/reliable_ordered_channel.h new file mode 100644 index 00000000..2ed86b78 --- /dev/null +++ b/include/iris/networking/channel/reliable_ordered_channel.h @@ -0,0 +1,85 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "networking/channel/channel.h" +#include "networking/socket.h" + +namespace iris +{ + +/** + * Implementation of Channel with the following guarantees: + * - Packets are in order + * - Packets are guaranteed to arrive + * - No gaps + * - No duplicates + * + * This is the strictest channel and effectively provides reliable delivery over + * an unreliable transport. + * + * e.g. + * received packets : 1, 1, 3, 2, 1, 5, 4 + * yielded packets : 1, 2, 3, 4, 5 + */ +class ReliableOrderedChannel : public Channel +{ + public: + /** + * Construct a new ReliableOrderedChannel. + */ + ReliableOrderedChannel(); + + // default + ~ReliableOrderedChannel() override = default; + + /** + * Enqueue a packet to be sent. + * + * @param packet + * Packet to be sent. + */ + void enqueue_send(Packet packet) override; + + /** + * Enqueue a received packet. + * + * @param packet + * Packet received. + */ + void enqueue_receive(Packet packet) override; + + /** + * Yield all packets to be sent, according to the channel guarantees. + * + * @returns + * Packets to be send. + */ + std::vector yield_send_queue() override; + + /** + * Yield all packets that have been received, according to the channel + * guarantees. + * + * @returns + * Packets received. + */ + std::vector yield_receive_queue() override; + + private: + /** The expected sequence number of the next packet. */ + std::uint16_t next_receive_seq_; + + /** The sequence number for the next sent packet. */ + std::uint16_t out_sequence_; +}; + +} diff --git a/include/iris/networking/channel/unreliable_sequenced_channel.h b/include/iris/networking/channel/unreliable_sequenced_channel.h new file mode 100644 index 00000000..d656c402 --- /dev/null +++ b/include/iris/networking/channel/unreliable_sequenced_channel.h @@ -0,0 +1,63 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "networking/channel/channel.h" +#include "networking/socket.h" + +namespace iris +{ + +/** + * Implementation of Channel with the following guarantees: + * - Packets are sequenced but may have gaps + * - No duplicates + * + * e.g. + * received packets : 1, 1, 3, 2, 1, 5 + * yielded packets : 1, 3, 5 + */ +class UnreliableSequencedChannel : public Channel +{ + public: + /** + * Construct a new UnreliableSequencedChannel. + */ + UnreliableSequencedChannel(); + + // default + ~UnreliableSequencedChannel() override = default; + + /** + * Enqueue a packet to be sent. + * + * @param packet + * Packet to be sent. + */ + void enqueue_send(Packet packet) override; + + /** + * Enqueue a received packet. + * + * @param packet + * Packet received. + */ + void enqueue_receive(Packet packet) override; + + private: + /** The minimum sequence number to yield for received packets. */ + std::uint16_t min_sequence_; + + /** The sequence number for the next sent packet. */ + std::uint16_t send_sequence_; +}; + +} diff --git a/include/iris/networking/channel/unreliable_unordered_channel.h b/include/iris/networking/channel/unreliable_unordered_channel.h new file mode 100644 index 00000000..933ceb2c --- /dev/null +++ b/include/iris/networking/channel/unreliable_unordered_channel.h @@ -0,0 +1,41 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "networking/channel/channel.h" + +namespace iris +{ + +/** + * Implementation of Channel with no guarantees. This is the most basic channel + * and is effectively a no-op for unreliable traffic. + */ +class UnreliableUnorderedChannel : public Channel +{ + public: + // default + ~UnreliableUnorderedChannel() override = default; + + /** + * Enqueue a packet to be sent. + * + * @param packet + * Packet to be sent. + */ + void enqueue_send(Packet packet) override; + + /** + * Enqueue a received packet. + * + * @param packet + * Packet received. + */ + void enqueue_receive(Packet packet) override; +}; + +} diff --git a/include/iris/networking/client_connection_handler.h b/include/iris/networking/client_connection_handler.h new file mode 100644 index 00000000..bda6044b --- /dev/null +++ b/include/iris/networking/client_connection_handler.h @@ -0,0 +1,105 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include + +#include "core/data_buffer.h" +#include "jobs/concurrent_queue.h" +#include "networking/channel/channel.h" +#include "networking/socket.h" + +namespace iris +{ + +/** + * This class provides an abstraction over the low-level handling of connecting + * to a server. + * It automatically handles: + * - making a connection + * - handshake + * - clock sync + * - sending/receiving data + * + * This is all done with the lightweight Packet protocol (see + * server_connection_handler.h for details on the protocol). + */ +class ClientConnectionHandler +{ + public: + /** + * Create a new ClientConnectionHandler. + * + * @param socker + * The underlying socket to use. + */ + explicit ClientConnectionHandler(std::unique_ptr socket); + + /** + * Try and read data from the supplied channel. + * + * @param channel_type + * Channel to read from. + * + * @returns + * DataBuffer of bytes if read succeeded, otherwise empty optional. + */ + std::optional try_read(ChannelType channel_type); + + /** + * Send data to the server on the supplied channel. + * + * @param data + * Data to send. + * + * @param channel_type + * Channel to send data on + */ + void send(const DataBuffer &data, ChannelType channel_type); + + void flush(); + + /** + * Unique id of the client (as set by server). + * + * @returns + * Server id. + */ + std::uint32_t id() const; + + /** + * Estimate of the lag between the client and server. This is the round + * trip time for a message to get to the server and back. Can change if + * the server issues a sync. + * + * @returns + * Estimate of lag. + */ + std::chrono::milliseconds lag() const; + + private: + /** Underlying socket. */ + std::unique_ptr socket_; + + /** Unique id of this client. */ + std::uint32_t id_; + + /** Estimate of lag between client and server. */ + std::chrono::milliseconds lag_; + + /** Map of channel types to channel objects. */ + std::map> channels_; + + /** Map of channel types to message queues. */ + std::map>> queues_; +}; + +} diff --git a/include/iris/networking/data_buffer_deserialiser.h b/include/iris/networking/data_buffer_deserialiser.h new file mode 100644 index 00000000..cd3a85ed --- /dev/null +++ b/include/iris/networking/data_buffer_deserialiser.h @@ -0,0 +1,171 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include + +#include "core/data_buffer.h" +#include "core/exception.h" +#include "core/quaternion.h" +#include "core/vector3.h" + +namespace iris +{ + +/** + * Class for deserialising types stored in a DataBuffer. This is the inverse + * operation to DataBufferSerialiser. + */ +class DataBufferDeserialiser +{ + public: + /** + * Construct a new DataBufferDeserialiser. + * + * @param buffer + * DataBuffer of serialised data. + */ + explicit DataBufferDeserialiser(DataBuffer buffer) + : buffer_(std::move(buffer)) + , cursor_(std::cbegin(buffer_)) + { + } + + /** + * Pop integral type. + * + * @returns + * Next element in buffer as supplied type. + */ + template > * = nullptr> + T pop() + { + const auto size = sizeof(T); + if (size > std::distance(cursor_, std::cend(buffer_))) + { + throw Exception("not enough data left"); + } + + T value{}; + std::memcpy(&value, std::addressof(*cursor_), size); + + cursor_ += size; + + return value; + } + + /** + * Pop enum. + * + * @returns + * Next element in buffer as supplied type. + */ + template > * = nullptr> + T pop() + { + using type = std::underlying_type_t; + + const auto value = pop(); + + return static_cast(value); + } + + /** + * Pop Vector3. + * + * @returns + * Next element in buffer as supplied type. + */ + template > * = nullptr> + T pop() + { + return T{pop(), pop(), pop()}; + } + + /** + * Pop Quaternion. + * + * @returns + * Next element in buffer as supplied type. + */ + template > * = nullptr> + T pop() + { + return T{pop(), pop(), pop(), pop()}; + } + + /** + * Pop DataBuffer. + * + * @returns + * Next element in buffer as supplied type. + */ + template > * = nullptr> + T pop() + { + const auto size = pop(); + + DataBuffer value(cursor_, cursor_ + size); + cursor_ += size; + + return value; + } + + /** + * Pop tuple of supplied types. + * + * @returns + * std::tuple of requested types from buffer. + */ + template + std::tuple pop_tuple() + { + std::tuple values; + + pop_tuple_impl<0u, std::tuple, Types...>(values); + + return values; + } + + private: + /** + * Helper recursive template method. Sets the next requested tuple + * element. + * + * @param values + * Tuple of elements to append to. + */ + template + void pop_tuple_impl(T &values) + { + // set next tuple element + std::get(values) = pop(); + + // recurse to set next element + pop_tuple_impl(values); + } + + /** + * Base method for template recursion. + */ + template + void pop_tuple_impl(T &) + { + } + + /** Buffer of serialised data. */ + DataBuffer buffer_; + + /** Iterator into buffer, where next element will be popped from. */ + DataBuffer::const_iterator cursor_; +}; + +} diff --git a/include/iris/networking/data_buffer_serialiser.h b/include/iris/networking/data_buffer_serialiser.h new file mode 100644 index 00000000..a8fc699d --- /dev/null +++ b/include/iris/networking/data_buffer_serialiser.h @@ -0,0 +1,114 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +#include "core/data_buffer.h" +#include "core/quaternion.h" +#include "core/vector3.h" + +namespace iris +{ + +/** + * Class for serialising types to a DataBuffer. + * + * Note this assumes little-endian. + */ +class DataBufferSerialiser +{ + public: + /** + * Get the serialised data. + * + * @returns + * DataBuffer of serialised data. + */ + DataBuffer data() const + { + return buffer_; + } + + /** + * Serialise an integral type. + * + * @param value + * Value to serialise. + */ + template > * = nullptr> + void push(T value) + { + const auto size = sizeof(T); + buffer_.resize(buffer_.size() + size); + std::memcpy(std::addressof(*(std::end(buffer_) - size)), &value, size); + } + + /** + * Serialise an enum. + * + * @param value + * Value to serialise. + */ + template > * = nullptr> + void push(T value) + { + using type = std::underlying_type_t; + + push(static_cast(value)); + } + + /** + * Serialise a Vector3. + * + * @param value + * Value to serialise. + */ + void push(const Vector3 &value) + { + push(value.x); + push(value.y); + push(value.z); + } + + /** + * Serialise a Quaternion. + * + * @param value + * Value to serialise. + */ + void push(const Quaternion &value) + { + push(value.x); + push(value.y); + push(value.z); + push(value.w); + } + + /** + * Serialise a DataBuffer. + * + * @param value + * Value to serialise. + */ + void push(const DataBuffer &value) + { + const auto size = value.size(); + push(static_cast(size)); + buffer_.resize(buffer_.size() + size); + std::memcpy(std::addressof(*(std::end(buffer_) - size)), value.data(), size); + } + + private: + /** Serialised data. */ + DataBuffer buffer_; +}; + +} diff --git a/include/iris/networking/networking.h b/include/iris/networking/networking.h new file mode 100644 index 00000000..4147fc48 --- /dev/null +++ b/include/iris/networking/networking.h @@ -0,0 +1,121 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +// this file abstracts away platform specific networking includes as we as +// several utility functions +// it should suffice to just include this file to use BSD socket functions + +#include + +#if defined(IRIS_PLATFORM_WIN32) +#include +#include +#pragma comment(lib, "Ws2_32.lib") +#include "networking/win32/winsock.h" +static iris::Winsock ws_init; +#else +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +#include "core/exception.h" + +namespace iris +{ + +#if defined(IRIS_PLATFORM_WIN32) + +using SocketHandle = SOCKET; +static std::function CloseSocket = ::closesocket; + +/** + * Set whether a socket should block on read. + * + * @param socket + * Handle to socket to change. + * + * @param blocking + * Whether socket should block or not. + */ +inline void set_blocking(SocketHandle socket, bool blocking) +{ + u_long mode = blocking ? 0 : 1; + if (::ioctlsocket(socket, FIONBIO, &mode) != NO_ERROR) + { + throw Exception("could not set blocking mode"); + } +} + +/** + * Check if the last read call would have blocked. i.e. no data was available. + * + * Note this is function is only valid following a read on a non-blocking + * socket. + * + * @returns + * True if read would have blocked, false otherwise. + */ +inline bool last_call_blocked() +{ + return ::WSAGetLastError() == WSAEWOULDBLOCK; +} + +#else + +using SocketHandle = int; +static std::function CloseSocket = ::close; +// this is already defined on windows so we define it again for consistency +#define INVALID_SOCKET -1 + +/** + * Set whether a socket should block on read. + * + * @param socket + * Handle to socket to change. + * + * @param blocking + * Whether socket should block or not. + */ +inline void set_blocking(SocketHandle socket, bool blocking) +{ + auto flags = ::fcntl(socket, F_GETFL, 0); + if (flags == -1) + { + throw Exception("could not get flags"); + } + + flags = blocking ? (flags & ~O_NONBLOCK) : (flags | O_NONBLOCK); + + if (::fcntl(socket, F_SETFL, flags) != 0) + { + throw Exception("could not set flags"); + } +} + +/** + * Check if the last read call would have blocked. i.e. no data was available. + * + * Note this is function is only valid following a read on a non-blocking + * socket. + * + * @returns + * True if read would have blocked, false otherwise. + */ +inline bool last_call_blocked() +{ + return errno == EWOULDBLOCK; +} +#endif + +} diff --git a/include/iris/networking/packet.h b/include/iris/networking/packet.h new file mode 100644 index 00000000..2be3201b --- /dev/null +++ b/include/iris/networking/packet.h @@ -0,0 +1,266 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +#include "core/data_buffer.h" +#include "networking/channel/channel_type.h" +#include "networking/packet_type.h" + +namespace iris +{ + +/** + * Class encapsulating a packet the engine will send/receive. This is a + * low-level primitive used to facilitate the basic protocol the engine + * provides. It is expected that a user will not create and send/receive these + * packets directly, but rather use other constructs in the engine to send their + * own game-specific protocol. + * + * The Packet consists of a header then a fixed body buffer, which can contain + * arbitrary data (may be smaller than the buffer size). + * + * +----------+ -. + * | type | | + * +----------+ | + * | channel | |- header + * +----------+ | + * | sequence | | + * .- +----------+ -' + * | | | + * | | | + * | | | + * | | body | + * | | | + * body buffer -| | | + * | | | + * | |~~~~~~~~~~|- body size + * | | | + * | | | + * | | | + * '- +----------+ + */ +class Packet +{ + public: + /** + * Construct an invalid Packet. All methods on an invalid packet should + * be considered undefined except: + * - is_valid + * - data + */ + Packet(); + + /** + * Construct a new Packet. + * + * @param type + * The type of packet. + * + * @param channel + * The channel the packet should be sent on. + * + * @param body + * The data of the packet, may be empty. + */ + Packet(PacketType type, ChannelType channel, const DataBuffer &body); + + /** + * Construct a new Packet from raw data. + * + * @param raw_data + * Raw Packet data + */ + explicit Packet(const DataBuffer &raw_packet); + + /** + * Get a pointer to the start of the packet. + * + * @returns + * Pointer to start of this Packet. + */ + const std::byte *data() const; + + /** + * Get a pointer to the start of the packet. + * + * @returns + * Pointer to start of this Packet. + */ + std::byte *data(); + + /** + * Get a pointer to the body of the packet. + * + * @returns + * Pointer to start of the Packet body. + */ + const std::byte *body() const; + + /** + * Get a pointer to the body of the packet. + * + * @returns + * Pointer to start of the Packet body. + */ + std::byte *body(); + + /** + * Get the contents of the body. + * + * @returns + * Body contents. + */ + DataBuffer body_buffer() const; + + /** + * Get the size of the packet i.e. sizeof(header) + sizeof(body). + * + * Note this may be less than sizeof(Packet) if the body buffer is not + * full. + * + * @returns + * Size of Packet that is filled. + */ + std::size_t packet_size() const; + + /** + * Get the size of the body i.e. how much of the body buffer is filled. + * + * @returns + * Size of body. + */ + std::size_t body_size() const; + + /** + * Get packet type. + * + * @returns + * Type of packet. + */ + PacketType type() const; + + /** + * Get channel type. + * + * @returns + * Type of channel. + */ + ChannelType channel() const; + + /** + * Check if Packet is valid. + * + * @returns + * True if packet is valid, otherwise false. + */ + bool is_valid() const; + + /** + * Get the sequence number of the Packet. + * + * @returns + * Sequence number. + */ + std::uint16_t sequence() const; + + /** + * Set the sequence number. + * + * @param sequence + * New sequence number. + */ + void set_sequence(std::uint16_t sequence); + + /** + * Equality operator. + * + * @param other + * Packet to check for equality. + * + * @returns + * True if both Packet objects are the same, otherwise false. + */ + bool operator==(const Packet &other) const; + + /** + * Inequality operator. + * + * @param other + * Packet to check for inequality. + * + * @returns + * True if both Packet objects are not the same, otherwise false. + */ + bool operator!=(const Packet &other) const; + + /** + * Write a Packet to a stream, useful for debugging. + * + * @param out + * Stream to write to. + * + * @param packet + * Packet to write to stream. + * + * @returns + * Reference to input stream. + */ + friend std::ostream &operator<<(std::ostream &out, const Packet &packet); + + private: + /** + * Internal struct for Packet header. + */ + struct Header + { + /** + * Construct a new Header. + * + * @param type + * Packet type. + * + * @param channel + * Channel type. + */ + Header(PacketType type, ChannelType channel) + : type(type) + , channel(channel) + , sequence(0u) + { + // check the header has no padding + static_assert(sizeof(Header) == sizeof(type) + sizeof(channel) + sizeof(sequence), "header has padding"); + } + + /** Type of packet. */ + PacketType type; + + /** Type of channel. */ + ChannelType channel; + + /** Sequence number. */ + std::uint16_t sequence; + }; + + /** Packet header. */ + Header header_; + + /** Packet body buffer. */ + std::byte body_[128 - sizeof(Header)]; + + // anything after here should be considered local bookkeeping and will + // not be transmitted when a Packet is sent. + + /** Size of body buffer used. */ + std::size_t size_; +}; + +} diff --git a/include/iris/networking/packet_type.h b/include/iris/networking/packet_type.h new file mode 100644 index 00000000..4f508e78 --- /dev/null +++ b/include/iris/networking/packet_type.h @@ -0,0 +1,29 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** + * Enumeration of packet types. + */ +enum class PacketType : std::uint8_t +{ + INVAlID, + HELLO, + CONNECTED, + DATA, + ACK, + SYNC_START, + SYNC_RESPONSE, + SYNC_FINISH +}; + +} diff --git a/include/iris/networking/server_connection_handler.h b/include/iris/networking/server_connection_handler.h new file mode 100644 index 00000000..885d8048 --- /dev/null +++ b/include/iris/networking/server_connection_handler.h @@ -0,0 +1,162 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "core/data_buffer.h" +#include "networking/channel/channel.h" +#include "networking/channel/channel_type.h" +#include "networking/server_socket.h" + +namespace iris +{ + +/** + * This class provides an abstraction over the low-level handling of + * connections. It automatically handles: + * - accepting connections + * - handshakes + * - clock syncs + * - sending/receiving data + * + * This is all done with the lightweight Packet protocol and Channels. This + * class can also be provided with callbacks for key events. + * + * Protocol: + * + * Handshake - must be performed when client connections. + * + * client server + * HELLO + * --------> + * + * CONNECTED + * [id] + * <-------- + * + * Data - this is sent via DATA packets, ACKs may be sent in response depending + * on the channel used. + * + * Sync - this allows the client to synchronise its clock with the server, + * always happens after handshake but my happen again if the server thinks the + * client is out of sync. + * + * client server + * SYNC_START + * <-------- + * + * SYNC_RESPONSE + * [client_time] + * --------> + * + * SYNC_START + * [client_time] + * [server_time] + * <-------- + */ +class ServerConnectionHandler +{ + public: + /** + * Callback for new connections + * + * @param id + * A unique id for this connection. + */ + using NewConnectionCallback = std::function; + + /** + * Callback for when a connection sends data. + * + * @param id + * Id of connection sending data. + * + * @param data + * The data send. + * + * @param channel + * The channel type the client sent the data on. + */ + using RecvCallback = std::function; + + /** + * Create a new ServerConnectionHandler. + * + * @param socket + * The underlying socket to use. + * + * @param new_connection + * Callback to fire when a new connection is created. + * + * @param recv + * Callback to fire when data is received. + */ + ServerConnectionHandler( + std::unique_ptr socket, + NewConnectionCallback new_connection, + RecvCallback recv); + + // defined in implementation + ~ServerConnectionHandler(); + + // deleted + ServerConnectionHandler(const ServerConnectionHandler &) = delete; + ServerConnectionHandler &operator=(const ServerConnectionHandler &) = delete; + + /** + * Updates the connection handler, processes all messages and fires all + * callbacks. This should be called regularly (e.g. from a game loop) + */ + void update(); + + /** + * Send data to a connection. + * + * @param id + * Id of connection to send data to. + * + * @param message + * Data to send. + * + * @param channel_type + * The channel to send the data through + */ + void send(std::size_t id, const DataBuffer &message, ChannelType channel_type); + + private: + // forward declare internal struct + struct Connection; + + /** Underlying socket. */ + std::unique_ptr socket_; + + /** New connection callback. */ + NewConnectionCallback new_connection_callback_; + + /** Received data callback. */ + RecvCallback recv_callback_; + + /** Start time of connection handler. */ + std::chrono::steady_clock::time_point start_; + + /** Map of connections to their unique id. */ + std::map> connections_; + + /** Mutex to control access to messages. */ + std::mutex mutex_; + + /** Collection of messages. */ + std::vector messages_; +}; + +} diff --git a/include/iris/networking/server_socket.h b/include/iris/networking/server_socket.h new file mode 100644 index 00000000..d541de66 --- /dev/null +++ b/include/iris/networking/server_socket.h @@ -0,0 +1,35 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "networking/server_socket_data.h" + +namespace iris +{ + +/** + * Interface for a server socket. This is a socket that will read data from all + * clients, returning read data as well as a Socket object to communicate back + * with them. + * + * See documentation in deriving classes for implementation specific caveats. + */ +class ServerSocket +{ + public: + virtual ~ServerSocket() = default; + + /** + * Block and wait for data. + * + * @returns + * A ServerSocketData for the read client and data. + */ + virtual ServerSocketData read() = 0; +}; + +} diff --git a/include/iris/networking/server_socket_data.h b/include/iris/networking/server_socket_data.h new file mode 100644 index 00000000..7e0b3234 --- /dev/null +++ b/include/iris/networking/server_socket_data.h @@ -0,0 +1,30 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "core/data_buffer.h" +#include "networking/socket.h" + +namespace iris +{ + +/** + * Struct for data returned from a ServerSocket read. + */ +struct ServerSocketData +{ + /** A Socket which can be used to communicate with the client. */ + Socket *client; + + /** The data read. */ + DataBuffer data; + + /** Whether this is a new connection or not. */ + bool new_connection; +}; + +} diff --git a/include/iris/networking/simulated_server_socket.h b/include/iris/networking/simulated_server_socket.h new file mode 100644 index 00000000..60aa6c34 --- /dev/null +++ b/include/iris/networking/simulated_server_socket.h @@ -0,0 +1,81 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "networking/server_socket.h" +#include "networking/server_socket_data.h" +#include "networking/simulated_socket.h" + +namespace iris +{ + +/** + * An adaptor for ServerSocket which can simulate different network conditions. + * Note that these conditions are compounded with any properties of the + * underlying Socket. + * + * Supports one clinet. + */ +class SimulatedServerSocket : public ServerSocket +{ + public: + /** + * Construct a new SimulatedServerSocket. + * + * @param delay + * The fixed delay for all packets. + * + * @param jitter + * The random variance in delay. All packets will be delayed by: + * delay + rand[-jitter, jitter] + * + * @param drop_rate + * The rate at which packets will be dropped, must be in the range + * [0.0, 1.0] -> [no packets dropped, all packets dropped] + * + * @param socket + * ServerSocket to adapt. Will be used for underlying communication, but + * with simulated conditions. + */ + SimulatedServerSocket( + std::chrono::milliseconds delay, + std::chrono::milliseconds jitter, + float drop_rate, + ServerSocket *socket); + + ~SimulatedServerSocket() override = default; + + /** + * Block and wait for data. + * + * @returns + * A ServerSocketData for the read client and data. + */ + ServerSocketData read() override; + + private: + /** Underlying socket. */ + ServerSocket *socket_; + + /** The single client. */ + std::unique_ptr client_; + + /** Simulated delay. */ + std::chrono::milliseconds delay_; + + /** Simulated jitter. */ + std::chrono::milliseconds jitter_; + + /** Simulated packet drop rate. */ + float drop_rate_; +}; + +} diff --git a/include/iris/networking/simulated_socket.h b/include/iris/networking/simulated_socket.h new file mode 100644 index 00000000..b0e84474 --- /dev/null +++ b/include/iris/networking/simulated_socket.h @@ -0,0 +1,113 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "jobs/concurrent_queue.h" +#include "networking/socket.h" + +namespace iris +{ + +/** + * An adaptor for Socket which can simulate different network conditions. Note + * that these conditions are compounded with any properties of the underlying + * Socket. + */ +class SimulatedSocket : public Socket +{ + public: + /** + * Construct a new SimulatedSocket. + * + * @param delay + * The fixed delay for all packets. + * + * @param jitter + * The random variance in delay. All packets will be delayed by: + * delay + rand[-jitter, jitter] + * + * @param drop_rate + * The rate at which packets will be dropped, must be in the range + * [0.0, 1.0] -> [no packets dropped, all packets dropped] + * + * @param socket + * Socket to adapt. Will be used for underlying communication, but with + * simulated conditions. + */ + SimulatedSocket(std::chrono::milliseconds delay, std::chrono::milliseconds jitter, float drop_rate, Socket *socket); + + // defined in implementation + ~SimulatedSocket() override; + + // deleted + SimulatedSocket(const SimulatedSocket &) = delete; + SimulatedSocket &operator=(const SimulatedSocket &) = delete; + + /** + * Try and read requested number bytes. Will return all bytes read up to + * count, but maybe less. + * + * @param count + * Maximum number of bytes to read. + * + * @returns + * DataBuffer of bytes if read succeeded, otherwise empty optional. + */ + std::optional try_read(std::size_t count) override; + + /** + * Block and read up to count bytes. May return less. + * + * @param count + * Maximum number of bytes to read. + * + * @returns + * DataBuffer of bytes read. + */ + DataBuffer read(std::size_t count) override; + + /** + * Write DataBuffer to socket. + * + * @param buffer + * Bytes to write. + */ + void write(const DataBuffer &buffer) override; + + /** + * Write bytes to socket. + * + * @param data + * Pointer to bytes to write. + * + * @param size + * Amount of bytes to write. + */ + void write(const std::byte *data, std::size_t size) override; + + private: + /** Packet delay. */ + std::chrono::milliseconds delay_; + + /** Delay jitter. */ + std::chrono::milliseconds jitter_; + + /** Packet drop rate. */ + float drop_rate_; + + /** Underlying socket. */ + Socket *socket_; + + /** Queue of data to send and when. */ + ConcurrentQueue> write_queue_; +}; + +} diff --git a/include/iris/networking/socket.h b/include/iris/networking/socket.h new file mode 100644 index 00000000..c3c2bd41 --- /dev/null +++ b/include/iris/networking/socket.h @@ -0,0 +1,78 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "core/data_buffer.h" + +namespace iris +{ + +/** + * Interface for a socket. This is an object that can read and write bytes + * from/to another socket object (possibly on a separate machine). + */ +class Socket +{ + public: + // default + virtual ~Socket() = default; + + /** + * Try and read count bytes if they are available (this should be a + * non-blocking call). + * + * Note that if not all requested bytes are available it is down to the + * implementation whether this is treated as error or just the bytes + * read are returned. + * + * @param count + * Amount of bytes to read. + * + * @returns + * DataBuffer of bytes if read succeeded, otherwise empty optional. + */ + virtual std::optional try_read(std::size_t count) = 0; + + /** + * Read count bytes (this should be a blocking call). + * + * Note that if not all requested bytes are available it is down to the + * implementation whether this is treated as error or just the bytes + * read are returned. + * + * @param count + * Amount of bytes to read. + * + * @returns + * DataBuffer of bytes read. + */ + virtual DataBuffer read(std::size_t count) = 0; + + /** + * Write DataBuffer to socket. + * + * @param buffer + * Bytes to write. + */ + virtual void write(const DataBuffer &buffer) = 0; + + /** + * Write bytes to socket. + * + * @param data + * Pointer to bytes to write. + * + * @param size + * Amount of bytes to write. + */ + virtual void write(const std::byte *data, std::size_t size) = 0; +}; + +} diff --git a/include/iris/networking/udp_server_socket.h b/include/iris/networking/udp_server_socket.h new file mode 100644 index 00000000..9d5dd874 --- /dev/null +++ b/include/iris/networking/udp_server_socket.h @@ -0,0 +1,71 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +#include "core/auto_release.h" +#include "networking/networking.h" +#include "networking/server_socket.h" +#include "networking/server_socket_data.h" + +namespace iris +{ + +/** + * Implementation of ServerSocket which accepts UDP connections. + * + * The ServerSocketData provides a socket for communicating back with the + * client. This behaves as any normal socket and can be read from and + * written to. + * + * Note that in general one should avoid reading from the returned client + * Socket object. This is because internally the client socket and this + * ServerSocket hold the same underlying network primitive. Which means reading + * from one could potentially interfere with the other. Best practice is to + * only every read from the UdpServerSocket and write back with the returned + * client Socket. + */ +class UdpServerSocket : public ServerSocket +{ + public: + /** + * Construct a new UdpServerSocket. + * + * @param address + * Address to listen to for connections. + * + * @param port + * Port to listen on. + */ + UdpServerSocket(const std::string &address, std::uint32_t port); + + UdpServerSocket(const UdpServerSocket &) = delete; + UdpServerSocket &operator=(const UdpServerSocket &) = delete; + + ~UdpServerSocket() override = default; + + /** + * Block and wait for data. + * + * @returns + * A ServerSocketData for the read client and data. + */ + ServerSocketData read() override; + + private: + /** Map of address to Socket for clients. */ + std::map> connections_; + + /** Underlying server socket. */ + AutoRelease socket_; +}; + +} diff --git a/include/iris/networking/udp_socket.h b/include/iris/networking/udp_socket.h new file mode 100644 index 00000000..429cbeed --- /dev/null +++ b/include/iris/networking/udp_socket.h @@ -0,0 +1,119 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include + +#include "core/auto_release.h" +#include "core/data_buffer.h" +#include "networking/networking.h" +#include "networking/socket.h" + +namespace iris +{ + +/** + * Implementation of Socket using UDP. This class simply sends UDP datagrams and + * makes no guarantees about deliveries or order. + */ +class UdpSocket : public Socket +{ + public: + /** + * Construct a new UdpSocket to the supplied address and port. + * + * @param address + * Address to communicate with. + * + * @param port + * Port on address to communicate with. + */ + UdpSocket(const std::string &address, std::uint16_t port); + + /** + * Construct a new UdpSocket from an existing BSD socket. This is non-owning + * and will not close the SocketHandle when it goes out of scope. + * + * This constructor annoyingly breaks the abstraction around the BSD socket + * primitives but is a necessary evil. + * + * @param socket_address + * BSD socket struct. + * + * @param socket_length + * Length (in bytes) of socket_address + * + * @param socket + * SocketHandle to take a non-owning copy of. + */ + UdpSocket(struct sockaddr_in socket_address, socklen_t socket_length, SocketHandle socket); + + // disabled + UdpSocket(const UdpSocket &) = delete; + UdpSocket &operator=(const UdpSocket &) = delete; + + // defined in implementation + ~UdpSocket() override = default; + + /** + * Try and read requested number bytes. Will return all bytes read up to + * count, but maybe less. + * + * @param count + * Maximum number of bytes to read. + * + * @returns + * DataBuffer of bytes if read succeeded, otherwise empty optional. + */ + std::optional try_read(std::size_t count) override; + + /** + * Block and read up to count bytes. May return less. + * + * @param count + * Maximum number of bytes to read. + * + * @returns + * DataBuffer of bytes read. + */ + DataBuffer read(std::size_t count) override; + + /** + * Write DataBuffer to socket. + * + * @param buffer + * Bytes to write. + */ + void write(const DataBuffer &buffer) override; + + /** + * Write bytes to socket. + * + * @param data + * Pointer to bytes to write. + * + * @param size + * Amount of bytes to write. + */ + void write(const std::byte *data, std::size_t size) override; + + private: + /** Socket wrapper. */ + AutoRelease socket_; + + /** BSD socket. */ + struct sockaddr_in address_; + + /** BSD socket length. */ + socklen_t address_length_; +}; + +} diff --git a/include/iris/networking/win32/winsock.h b/include/iris/networking/win32/winsock.h new file mode 100644 index 00000000..75280c5d --- /dev/null +++ b/include/iris/networking/win32/winsock.h @@ -0,0 +1,27 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace iris +{ +/** + * RAII class to initialise and cleanup winsock. + */ +class Winsock +{ + public: + /** + * Initialise winsock. + */ + Winsock(); + + /** + * Cleanup winsock. + */ + ~Winsock(); +}; +} diff --git a/include/iris/physics/basic_character_controller.h b/include/iris/physics/basic_character_controller.h new file mode 100644 index 00000000..9f8b6485 --- /dev/null +++ b/include/iris/physics/basic_character_controller.h @@ -0,0 +1,158 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "core/quaternion.h" +#include "core/vector3.h" +#include "physics/character_controller.h" +#include "physics/rigid_body.h" + +namespace iris +{ + +class PhysicsSystem; + +/** + * Implementation of CharacterController for a basic FPS character controller. + * Uses a capsule shape for character. + */ +class BasicCharacterController : public CharacterController +{ + public: + /** + * Create a BasicCharacterController. + * + * @param physics_system + * Pointer to physics_system that owns this controller. + */ + explicit BasicCharacterController(PhysicsSystem *physics_system); + + /** + * Destructor. + */ + ~BasicCharacterController() override; + + /** + * Set the direction the character is walking. Should be a normalised + * vector. + * + * @param direction + * Direction character is moving. + */ + void set_walk_direction(const Vector3 &direction) override; + + /** + * Get position of character in the world. + * + * @returns + * World coordinates of character. + */ + Vector3 position() const override; + + /** + * Get orientation of character. + * + * @returns + * Orientation of character + */ + Quaternion orientation() const override; + + /** + * Get linear velocity. + * + * @returns + * Linear velocity. + */ + Vector3 linear_velocity() const override; + + /** + * Get angular velocity. + * + * @returns + * Angular velocity. + */ + Vector3 angular_velocity() const override; + + /** + * Set linear velocity. + * + * @param linear_velocity + * New linear velocity. + */ + void set_linear_velocity(const Vector3 &linear_velocity) override; + + /** + * Set angular velocity. + * + * @param angular_velocity + * New angular velocity. + */ + void set_angular_velocity(const Vector3 &angular_velocity) override; + + /** + * Set speed of character. + * + * @param speed + * New speed. + */ + void set_speed(float speed) override; + + /** + * Reposition character. + * + * @param position + * New position. + * + * @param orientation + * New orientation. + */ + void reposition(const Vector3 &position, const Quaternion &orientation) override; + + /** + * Make the character jump. + */ + void jump() override; + + /** + * Check if character is standing on the ground. + * + * @returns + * True if character is on a surface, false otherwise. + */ + bool on_ground() const override; + + /** + * Get the underlying RigidBody. + * + * @returns + * Underlying RigidBody. + */ + RigidBody *rigid_body() const override; + + /** + * Set the collision shape for the controller. + * + * @param collision_shape + * New collision shape. + */ + void set_collision_shape(CollisionShape *collision_shape) override; + + private: + /** Speed of character. */ + float speed_; + + /* Mass of character. */ + float mass_; + + /** Physics system. */ + PhysicsSystem *physics_system_; + + /** Underlying rigid body, */ + RigidBody *body_; +}; + +} diff --git a/include/iris/physics/bullet/bullet_box_collision_shape.h b/include/iris/physics/bullet/bullet_box_collision_shape.h new file mode 100644 index 00000000..d620fd21 --- /dev/null +++ b/include/iris/physics/bullet/bullet_box_collision_shape.h @@ -0,0 +1,48 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include + +#include "core/vector3.h" +#include "physics/bullet/bullet_collision_shape.h" + +namespace iris +{ + +/** + * Implementation of CollisionShape for a box. + */ +class BulletBoxCollisionShape : public BulletCollisionShape +{ + public: + /** + * Construct a new BoxCollisionShape + * + * @param half_size + * The extends from the center of the box which define its size. + */ + explicit BulletBoxCollisionShape(const Vector3 &half_size); + + ~BulletBoxCollisionShape() override = default; + + /** + * Get a handle to the bullet object. + * + * @returns + * Bullet object. + */ + btCollisionShape *handle() const override; + + private: + /** Bullet collision shape. */ + std::unique_ptr shape_; +}; + +} diff --git a/include/iris/physics/bullet/bullet_capsule_collision_shape.h b/include/iris/physics/bullet/bullet_capsule_collision_shape.h new file mode 100644 index 00000000..cd0775fc --- /dev/null +++ b/include/iris/physics/bullet/bullet_capsule_collision_shape.h @@ -0,0 +1,50 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include + +#include "physics/bullet/bullet_collision_shape.h" + +namespace iris +{ + +/** + * Implementation of CollisionShape for a capsule. + */ +class BulletCapsuleCollisionShape : public BulletCollisionShape +{ + public: + /** + * Construct new CapsuleCollisionShape + * + * @param width + * Diameter of capsule. + * + * @param height + * Height of capsule. + */ + BulletCapsuleCollisionShape(float width, float height); + + ~BulletCapsuleCollisionShape() override = default; + + /** + * Get a handle to the bullet object. + * + * @returns + * Bullet object. + */ + btCollisionShape *handle() const override; + + private: + /** Bullet collision shape. */ + std::unique_ptr shape_; +}; + +} diff --git a/include/iris/physics/bullet/bullet_collision_shape.h b/include/iris/physics/bullet/bullet_collision_shape.h new file mode 100644 index 00000000..93f45c98 --- /dev/null +++ b/include/iris/physics/bullet/bullet_collision_shape.h @@ -0,0 +1,33 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "physics/collision_shape.h" + +namespace iris +{ + +/** + * Implementation of CollisionShape for bullet. + */ +class BulletCollisionShape : public CollisionShape +{ + public: + ~BulletCollisionShape() override = default; + + /** + * Get a handle to the bullet object. + * + * @returns + * Bullet object. + */ + virtual btCollisionShape *handle() const = 0; +}; + +} diff --git a/include/iris/physics/bullet/bullet_physics_manager.h b/include/iris/physics/bullet/bullet_physics_manager.h new file mode 100644 index 00000000..7a721b6e --- /dev/null +++ b/include/iris/physics/bullet/bullet_physics_manager.h @@ -0,0 +1,48 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "core/vector3.h" +#include "physics/basic_character_controller.h" +#include "physics/bullet/bullet_physics_system.h" +#include "physics/collision_shape.h" +#include "physics/physics_manager.h" +#include "physics/rigid_body.h" +#include "physics/rigid_body_type.h" + +namespace iris +{ + +/** + * Implementation of PhysicsManager for bullet. + */ +class BulletPhysicsManager : public PhysicsManager +{ + public: + ~BulletPhysicsManager() override = default; + + /** + * Create a new PhysicsSystem. + */ + PhysicsSystem *create_physics_system() override; + + /** + * Get the currently active PhysicsSystem. + * + * @returns + * Pointer to the current PhysicsSystem, nullptr if one does not exist. + */ + PhysicsSystem *current_physics_system() override; + + private: + /** Current physics system. */ + std::unique_ptr physics_system_; +}; + +} diff --git a/include/iris/physics/bullet/bullet_physics_system.h b/include/iris/physics/bullet/bullet_physics_system.h new file mode 100644 index 00000000..a084ce1c --- /dev/null +++ b/include/iris/physics/bullet/bullet_physics_system.h @@ -0,0 +1,225 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "core/quaternion.h" +#include "core/vector3.h" +#include "physics/bullet/bullet_collision_shape.h" +#include "physics/bullet/debug_draw.h" +#include "physics/character_controller.h" +#include "physics/collision_shape.h" +#include "physics/physics_system.h" +#include "physics/rigid_body.h" + +namespace iris +{ + +/** + * Implementation of PhysicsSystem for bullet. + */ +class BulletPhysicsSystem : public PhysicsSystem +{ + public: + /** + * Construct a new BulletPhysicsSystem. + */ + BulletPhysicsSystem(); + + ~BulletPhysicsSystem() override; + + // disabled + BulletPhysicsSystem(const BulletPhysicsSystem &) = delete; + BulletPhysicsSystem &operator=(const BulletPhysicsSystem &) = delete; + + /** + * Step the physics system by the supplied time. + * + * @param time_step + * The amount of time to simulate. + */ + void step(std::chrono::milliseconds time_step) override; + + /** + * Create a RigidBody and add it to the simulation. + * + * @param position + * Position in world space. + * + * @param collision_shape + * The shape that defined the rigid body, this is used for collision + * detection/response. + * + * @param type + * The type of rigid body, this effects how this body interacts with + * others. + * + * @returns + * A pointer to the newly created RigidBody. + */ + RigidBody *create_rigid_body(const Vector3 &position, CollisionShape *collision_shape, RigidBodyType type) override; + + /** + * Create a CharacterController and add it to the simulation. + * + * @returns + * A pointer to the newly created CharacterController. + */ + CharacterController *create_character_controller() override; + + /** + * Create a CollisionShape for a box. + * + * @param half_size + * The extends from the center of the box which define its size. + * + * @returns + * Pointer to newly created CollisionShape. + */ + CollisionShape *create_box_collision_shape(const Vector3 &half_size) override; + + /** + * Create a CollisionShape for a capsule. + * + * @param width + * Diameter of capsule. + * + * @param height + * Height of capsule. + * + * @returns + * Pointer to newly created CollisionShape. + */ + CollisionShape *create_capsule_collision_shape(float width, float height) override; + + /** + * Remove a body from the physics system. + * + * This will release all resources for the body, using the handle after this + * call is undefined. + * + * @param body + * Body to remove. + */ + void remove(RigidBody *body) override; + + /** + * Character controller a body from the physics system. + * + * This will release all resources for the character, using the handle after + * this call is undefined. + * + * @param body + * Body to remove. + */ + void remove(CharacterController *charaacter) override; + + /** + * Cast a ray into physics engine world. + * + * @param origin + * Origin of ray. + * + * @param direction. + * Direction of ray. + * + * @returns + * If ray hits an object then a tuple [object hit, point of intersection], + * else empty optional. + */ + std::optional> ray_cast(const Vector3 &origin, const Vector3 &direction) + const override; + + /** + * Add a body to be excluded from ray_casts + * + * @param body + * Body to ignore. + */ + void ignore_in_raycast(RigidBody *body) override; + + /** + * Save the current state of the simulation. + * + * Note that depending on the implementation this may be a "best guess" + * state save. Restoring isn't guaranteed to produce identical results + * although it should be close enough. + * + * @returns + * Saved state. + */ + std::unique_ptr save() override; + + /** + * Load saved state. This will restore the simulation to that of the + * supplied state. + * + * See save() comments for details of limitations. + * + * @param state + * State to restore from. + */ + void load(const PhysicsState *state) override; + + /** + * Enable debug rendering. This should only be called once. + * + * @param entity. + * The RenderEntity to store debug render data in. + */ + void enable_debug_draw(RenderEntity *entity) override; + + private: + /** Bullet interface for detecting AABB overlapping pairs. */ + std::unique_ptr broadphase_; + + /** + * Bullet callback for adding and removing overlapping pairs from the + * broadphase. + */ + std::unique_ptr ghost_pair_callback_; + + /** Bullet collision config object, */ + std::unique_ptr collision_config_; + + /** Object containing algorithms for handling collision pairs. */ + std::unique_ptr collision_dispatcher_; + + /** Bullet constraint solver. */ + std::unique_ptr solver_; + + /** Bullet simulated world. */ + std::unique_ptr world_; + + /** Collection of rigid bodies. */ + std::vector> bodies_; + + /** Collection of collision objects to be ignored during raycasts. */ + std::set ignore_; + + /** Collection of character controllers. */ + std::vector> character_controllers_; + + /** DebugDraw object. */ + std::unique_ptr debug_draw_; + + /** Collection of collision shapes. */ + std::vector> collision_shapes_; +}; + +} diff --git a/include/iris/physics/bullet/bullet_rigid_body.h b/include/iris/physics/bullet/bullet_rigid_body.h new file mode 100644 index 00000000..86e55044 --- /dev/null +++ b/include/iris/physics/bullet/bullet_rigid_body.h @@ -0,0 +1,190 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include +#include + +#include "core/quaternion.h" +#include "core/vector3.h" +#include "physics/bullet/bullet_collision_shape.h" +#include "physics/rigid_body.h" +#include "physics/rigid_body_type.h" + +namespace iris +{ + +/** + * Implementation of RigidBody for bullet. + */ +class BulletRigidBody : public RigidBody +{ + public: + /** + * Construct a new BulletRigidBody. + * + * @param position + * Position in world space. + * + * @param collision_shape + * The shape that defined the rigid body, this is used for collision + * detection/response. + * + * @param type + * The type of rigid body, this effects how this body interacts with + * others. + */ + BulletRigidBody(const Vector3 &position, BulletCollisionShape *collision_shape, RigidBodyType type); + + ~BulletRigidBody() override = default; + + /** + * Get position of rigid body centre of mass. + * + * @returns + * Rigid body position. + */ + Vector3 position() const override; + + /** + * Get orientation of rigid body. + * + * @returns + * Rigid body orientation. + */ + Quaternion orientation() const override; + + /** + * Get linear velocity. + * + * This is only valid for non GHOST type rigid bodies. + * + * @returns + * Linear velocity. + */ + Vector3 linear_velocity() const override; + + /** + * Get angular velocity. + * + * This is only valid for non GHOST type rigid bodies. + * + * @returns + * Angular velocity. + */ + Vector3 angular_velocity() const override; + + /** + * Set linear velocity. + * + * This is only valid for non GHOST type rigid bodies. + * + * @param linear_velocity + * New linear velocity. + */ + void set_linear_velocity(const Vector3 &linear_velocity) override; + + /** + * Set angular velocity. + * + * This is only valid for non GHOST type rigid bodies. + * + * @param angular_velocity + * New angular velocity. + */ + void set_angular_velocity(const Vector3 &angular_velocity) override; + + /** + * Reposition rigid body. + * + * @param position + * New position. + * + * @param orientation + * New orientation. + */ + void reposition(const Vector3 &position, const Quaternion &orientation) override; + + /** + * Get the name of the rigid body. This is an optional trait and will return + * an empty string if a name has not already been set. + * + * @returns + * Optional name of rigid body. + */ + std::string name() const override; + + /** + * Set name. + * + * @param name + * New name. + */ + void set_name(const std::string &name) override; + + /** + * Get type. + * + * @returns + * Type of rigid body. + */ + RigidBodyType type() const override; + + /** + * Pointer to collision shape. + * + * @returns + * Collision shape. + */ + CollisionShape *collision_shape() const override; + + /** + * Set collision shape. + * + * @param collision_shape + * New collision shape. + */ + void set_collision_shape(CollisionShape *collision_shape) override; + + /** + * Apply an impulse (at the centre of mass). + * + * @param impulse + * Impulse to apply. + */ + void apply_impulse(const Vector3 &impulse) override; + + /** + * Get a handle to the bullet object. + * + * @returns + * Bullet object. + */ + btCollisionObject *handle() const; + + private: + /** Name of rigid body.*/ + std::string name_; + + /** Type of rigid body. */ + RigidBodyType type_; + + /** Collision shape of rigid body. */ + BulletCollisionShape *collision_shape_; + + /** Bullet collision object. */ + std::unique_ptr body_; + + /** Bullet motion state. */ + std::unique_ptr motion_state_; +}; + +} diff --git a/include/iris/physics/bullet/debug_draw.h b/include/iris/physics/bullet/debug_draw.h new file mode 100644 index 00000000..8df52c5f --- /dev/null +++ b/include/iris/physics/bullet/debug_draw.h @@ -0,0 +1,79 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include +#include + +#include "core/colour.h" +#include "core/vector3.h" +#include "graphics/render_entity.h" + +namespace iris +{ + +/** + * Implementation of bullets btIDebugDraw. This class enables bullet debug + * information to be rendered with the engine. + * + * The btIDebugDraw was designed for immediate mode rendering and so the methods + * will be continuously called with a series of elements to render. In order to + * fit in with the design of this engine all these elements are buffered. A + * render() method is exposed to process all the data sent by bullet and pass + * onto the rendering part of the engine. + */ +class DebugDraw : public ::btIDebugDraw +{ + public: + /** + * Construct a new DebugDraw. + */ + explicit DebugDraw(RenderEntity *entity); + + // default + ~DebugDraw() override = default; + + // the following methods are part of the bullet interface, see bullet + // documentation for details + // note they are not all implemented + + void drawLine(const ::btVector3 &from, const ::btVector3 &to, const ::btVector3 &colour) override; + + void drawContactPoint( + const ::btVector3 &PointOnB, + const ::btVector3 &normalOnB, + ::btScalar distance, + int lifeTime, + const ::btVector3 &color) override; + + void reportErrorWarning(const char *warningString) override; + + void draw3dText(const ::btVector3 &location, const char *textString) override; + + void setDebugMode(int debugMode) override; + + int getDebugMode() const override; + + /** + * Render all the elements from the above calls. + */ + void render(); + + private: + /** Collection of vertices to render. */ + std::vector> verticies_; + + /** A render entity for all debug shapes. */ + RenderEntity *entity_; + + /** Bullet specific debug mode. */ + int debug_mode_; +}; + +} diff --git a/include/iris/physics/character_controller.h b/include/iris/physics/character_controller.h new file mode 100644 index 00000000..af569f2b --- /dev/null +++ b/include/iris/physics/character_controller.h @@ -0,0 +1,133 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "core/quaternion.h" +#include "core/vector3.h" +#include "physics/collision_shape.h" + +namespace iris +{ + +class RigidBody; + +/** + * Interface for a character controller. Deriving classes should use this + * interface to implement how a character should move for their game. + */ +class CharacterController +{ + public: + virtual ~CharacterController() = default; + + /** + * Set the direction the character is walking. Should be a normalised + * vector. + * + * @param direction + * Direction character is moving. + */ + virtual void set_walk_direction(const Vector3 &direction) = 0; + + /** + * Get position of character in the world. + * + * @returns + * World coordinates of character. + */ + virtual Vector3 position() const = 0; + + /** + * Get orientation of character. + * + * @returns + * Orientation of character + */ + virtual Quaternion orientation() const = 0; + + /** + * Get linear velocity. + * + * @returns + * Linear velocity. + */ + virtual Vector3 linear_velocity() const = 0; + + /** + * Get angular velocity. + * + * @returns + * Angular velocity. + */ + virtual Vector3 angular_velocity() const = 0; + + /** + * Set linear velocity. + * + * @param linear_velocity + * New linear velocity. + */ + virtual void set_linear_velocity(const Vector3 &linear_velocity) = 0; + + /** + * Set angular velocity. + * + * @param angular_velocity + * New angular velocity. + */ + virtual void set_angular_velocity(const Vector3 &angular_velocity) = 0; + + /** + * Set speed of character. + * + * @param speed + * New speed. + */ + virtual void set_speed(float speed) = 0; + + /** + * Reposition character. + * + * @param position + * New position. + * + * @param orientation + * New orientation. + */ + virtual void reposition(const Vector3 &position, const Quaternion &orientation) = 0; + + /** + * Make the character jump. + */ + virtual void jump() = 0; + + /** + * Check if character is standing on the ground. + * + * @returns + * True if character is on a surface, false otherwise. + */ + virtual bool on_ground() const = 0; + + /** + * Get the underlying RigidBody. + * + * @returns + * Underlying RigidBody. + */ + virtual RigidBody *rigid_body() const = 0; + + /** + * Set the collision shape for the controller. + * + * @param collision_shape + * New collision shape. + */ + virtual void set_collision_shape(CollisionShape *collision_shape) = 0; +}; + +} diff --git a/include/iris/physics/collision_shape.h b/include/iris/physics/collision_shape.h new file mode 100644 index 00000000..33cb7ea1 --- /dev/null +++ b/include/iris/physics/collision_shape.h @@ -0,0 +1,24 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** + * This is an interface for 3D shape used for collision detection and + * resolution. + */ +class CollisionShape +{ + public: + virtual ~CollisionShape() = default; +}; + +} diff --git a/include/iris/physics/physics_manager.h b/include/iris/physics/physics_manager.h new file mode 100644 index 00000000..84a4549a --- /dev/null +++ b/include/iris/physics/physics_manager.h @@ -0,0 +1,36 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace iris +{ + +class PhysicsSystem; + +/** + * Interface for a class which creates PhysicsSystem objects. + */ +class PhysicsManager +{ + public: + virtual ~PhysicsManager() = default; + + /** + * Create a new PhysicsSystem. + */ + virtual PhysicsSystem *create_physics_system() = 0; + + /** + * Get the currently active PhysicsSystem. + * + * @returns + * Pointer to the current PhysicsSystem, nullptr if one does not exist. + */ + virtual PhysicsSystem *current_physics_system() = 0; +}; + +} diff --git a/include/iris/physics/physics_system.h b/include/iris/physics/physics_system.h new file mode 100644 index 00000000..0593aecc --- /dev/null +++ b/include/iris/physics/physics_system.h @@ -0,0 +1,187 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "core/quaternion.h" +#include "core/vector3.h" +#include "physics/character_controller.h" +#include "physics/collision_shape.h" +#include "physics/rigid_body.h" + +namespace iris +{ + +class RenderEntity; + +/** + * Interface for a class which stores the current state of a PhysicsSystem. + */ +struct PhysicsState +{ + virtual ~PhysicsState() = default; +}; + +/** + * Interface for a class which can manage and simulate a physics world. + */ +class PhysicsSystem +{ + public: + PhysicsSystem() = default; + + virtual ~PhysicsSystem() = default; + + // disabled + PhysicsSystem(const PhysicsSystem &) = delete; + PhysicsSystem &operator=(const PhysicsSystem &) = delete; + + /** + * Step the physics system by the supplied time. + * + * @param time_step + * The amount of time to simulate. + */ + virtual void step(std::chrono::milliseconds time_step) = 0; + + /** + * Create a RigidBody and add it to the simulation. + * + * @param position + * Position in world space. + * + * @param collision_shape + * The shape that defined the rigid body, this is used for collision + * detection/response. + * + * @param type + * The type of rigid body, this effects how this body interacts with + * others. + * + * @returns + * A pointer to the newly created RigidBody. + */ + virtual RigidBody *create_rigid_body( + const Vector3 &position, + CollisionShape *collision_shape, + RigidBodyType type) = 0; + + /** + * Create a CharacterController and add it to the simulation. + * + * @returns + * A pointer to the newly created CharacterController. + */ + virtual CharacterController *create_character_controller() = 0; + + /** + * Create a CollisionShape for a box. + * + * @param half_size + * The extends from the center of the box which define its size. + * + * @returns + * Pointer to newly created CollisionShape. + */ + virtual CollisionShape *create_box_collision_shape(const Vector3 &half_size) = 0; + + /** + * Create a CollisionShape for a capsule. + * + * @param width + * Diameter of capsule. + * + * @param height + * Height of capsule. + * + * @returns + * Pointer to newly created CollisionShape. + */ + virtual CollisionShape *create_capsule_collision_shape(float width, float height) = 0; + + /** + * Remove a body from the physics system. + * + * This will release all resources for the body, using the handle after this + * call is undefined. + * + * @param body + * Body to remove. + */ + virtual void remove(RigidBody *body) = 0; + + /** + * Character controller a body from the physics system. + * + * This will release all resources for the character, using the handle after + * this call is undefined. + * + * @param body + * Body to remove. + */ + virtual void remove(CharacterController *charaacter) = 0; + + /** + * Cast a ray into physics engine world. + * + * @param origin + * Origin of ray. + * + * @param direction. + * Direction of ray. + * + * @returns + * If ray hits an object then a tuple [object hit, point of intersection], + * else empty optional. + */ + virtual std::optional> ray_cast(const Vector3 &origin, const Vector3 &direction) + const = 0; + + /** + * Add a body to be excluded from ray_casts + * + * @param body + * Body to ignore. + */ + virtual void ignore_in_raycast(RigidBody *body) = 0; + + /** + * Save the current state of the simulation. + * + * Note that depending on the implementation this may be a "best guess" + * state save. Restoring isn't guaranteed to produce identical results + * although it should be close enough. + * + * @returns + * Saved state. + */ + virtual std::unique_ptr save() = 0; + + /** + * Load saved state. This will restore the simulation to that of the + * supplied state. + * + * See save() comments for details of limitations. + * + * @param state + * State to restore from. + */ + virtual void load(const PhysicsState *state) = 0; + + /** + * Enable debug rendering. This should only be called once. + * + * @param entity. + * The RenderEntity to store debug render data in. + */ + virtual void enable_debug_draw(RenderEntity *entity) = 0; +}; + +} diff --git a/include/iris/physics/rigid_body.h b/include/iris/physics/rigid_body.h new file mode 100644 index 00000000..94f48116 --- /dev/null +++ b/include/iris/physics/rigid_body.h @@ -0,0 +1,148 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "core/quaternion.h" +#include "core/vector3.h" +#include "physics/collision_shape.h" +#include "physics/rigid_body_type.h" + +namespace iris +{ + +/** + * Interface for a rigid body, a physics entity that can be added to the + * physics systems and simulated. It can collide and interact with other rigid + * bodies. + */ +class RigidBody +{ + public: + virtual ~RigidBody() = default; + + /** + * Get position of rigid body centre of mass. + * + * @returns + * Rigid body position. + */ + virtual Vector3 position() const = 0; + + /** + * Get orientation of rigid body. + * + * @returns + * Rigid body orientation. + */ + virtual Quaternion orientation() const = 0; + + /** + * Get linear velocity. + * + * This is only valid for non GHOST type rigid bodies. + * + * @returns + * Linear velocity. + */ + virtual Vector3 linear_velocity() const = 0; + + /** + * Get angular velocity. + * + * This is only valid for non GHOST type rigid bodies. + * + * @returns + * Angular velocity. + */ + virtual Vector3 angular_velocity() const = 0; + + /** + * Set linear velocity. + * + * This is only valid for non GHOST type rigid bodies. + * + * @param linear_velocity + * New linear velocity. + */ + virtual void set_linear_velocity(const Vector3 &linear_velocity) = 0; + + /** + * Set angular velocity. + * + * This is only valid for non GHOST type rigid bodies. + * + * @param angular_velocity + * New angular velocity. + */ + virtual void set_angular_velocity(const Vector3 &angular_velocity) = 0; + + /** + * Reposition rigid body. + * + * @param position + * New position. + * + * @param orientation + * New orientation. + */ + virtual void reposition(const Vector3 &position, const Quaternion &orientation) = 0; + + /** + * Get the name of the rigid body. This is an optional trait and will return + * an empty string if a name has not already been set. + * + * @returns + * Optional name of rigid body. + */ + virtual std::string name() const = 0; + + /** + * Set name. + * + * @param name + * New name. + */ + virtual void set_name(const std::string &name) = 0; + + /** + * Get type. + * + * @returns + * Type of rigid body. + */ + virtual RigidBodyType type() const = 0; + + /** + * Pointer to collision shape. + * + * @returns + * Collision shape. + */ + virtual CollisionShape *collision_shape() const = 0; + + /** + * Set collision shape. + * + * @param collision_shape + * New collision shape. + */ + virtual void set_collision_shape(CollisionShape *collision_shape) = 0; + + /** + * Apply an impulse (at the centre of mass). + * + * @param impulse + * Impulse to apply. + */ + virtual void apply_impulse(const Vector3 &impulse) = 0; +}; + +} diff --git a/include/iris/physics/rigid_body_type.h b/include/iris/physics/rigid_body_type.h new file mode 100644 index 00000000..c43affb5 --- /dev/null +++ b/include/iris/physics/rigid_body_type.h @@ -0,0 +1,38 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +namespace iris +{ + +/** + * Enumeration of possible rigid body types. + */ +enum class RigidBodyType : std::uint32_t +{ + /** + * A rigid body that is simulated as part of the physics engine and + * approximates a real world object (forces, velocity, collision, etc.). + */ + NORMAL, + + /** + * A rigid body that does not move. Other non static rigid bodies will + * collide with it. + */ + STATIC, + + /** + * A rigid body that can be moved and detects collision, although it does + * not perform collision resolution. + */ + GHOST +}; + +} diff --git a/media/logo.png b/media/logo.png new file mode 100644 index 00000000..d2fd4c97 Binary files /dev/null and b/media/logo.png differ diff --git a/media/physics.png b/media/physics.png new file mode 100644 index 00000000..32b43244 Binary files /dev/null and b/media/physics.png differ diff --git a/media/zombie.png b/media/zombie.png new file mode 100644 index 00000000..a811187d Binary files /dev/null and b/media/zombie.png differ diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt new file mode 100644 index 00000000..9d556ef4 --- /dev/null +++ b/samples/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory("sample_browser") +add_subdirectory("jobs") +add_subdirectory("window") diff --git a/samples/jobs/CMakeLists.txt b/samples/jobs/CMakeLists.txt new file mode 100644 index 00000000..3953b5ab --- /dev/null +++ b/samples/jobs/CMakeLists.txt @@ -0,0 +1,10 @@ +add_executable(jobs main.cpp) + +target_include_directories(jobs PRIVATE ${stb_SOURCE_DIR}) + +target_link_libraries(jobs iris) + +if(IRIS_PLATFORM MATCHES "WIN32") + set_target_properties(jobs PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDebug") +endif() + diff --git a/samples/jobs/main.cpp b/samples/jobs/main.cpp new file mode 100644 index 00000000..f23b3848 --- /dev/null +++ b/samples/jobs/main.cpp @@ -0,0 +1,290 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include +#define _USE_MATH_DEFINES +#include +#include +#include +#include + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include + +#include "core/colour.h" +#include "core/exception.h" +#include "core/root.h" +#include "core/start.h" +#include "core/vector3.h" +#include "jobs/job.h" +#include "jobs/job_system_manager.h" +#include "log/log.h" + +struct Sphere; + +// helpful globals +std::random_device rd; +std::mt19937 generator(rd()); +static std::vector scene; + +/** + * Simple ray class. + */ +struct Ray +{ + Ray(const iris::Vector3 &origin, const iris::Vector3 &direction) + : origin(origin) + , direction(direction) + { + } + + iris::Vector3 origin; + iris::Vector3 direction; +}; + +/** + * Simple sphere class. + */ +struct Sphere +{ + Sphere( + const iris::Vector3 &origin, + float radius, + const iris::Colour &colour) + : origin(origin) + , radius(radius) + , colour(colour) + { + } + + std::tuple intersects( + const Ray &ray) const + { + auto L = origin - ray.origin; + auto tca = L.dot(ray.direction); + auto d2 = L.dot(L) - tca * tca; + if (d2 > radius * radius) + { + return {std::numeric_limits::max(), {}, {}}; + } + + auto thc = sqrtf(radius * radius - d2); + auto t0 = tca - thc; + auto t1 = tca + thc; + if (t0 < 0) + { + t0 = t1; + } + + if (t0 < 0) + { + return {std::numeric_limits::max(), {}, {}}; + } + + auto q = ray.origin + (ray.direction * t0); + return {t0, q, iris::Vector3::normalise(q - origin)}; + } + + iris::Vector3 origin; + float radius; + iris::Colour colour; + bool is_metal = false; + + float rougness = 0.0f; +}; + +iris::Vector3 random_unit_vector() +{ + std::uniform_real_distribution dist1(0.0f, 1.0f); + float z = dist1(generator) * 2.0f - 1.0f; + float a = dist1(generator) * 2.0f * M_PI; + float r = sqrtf(1.0f - z * z); + float x = r * cosf(a); + float y = r * sinf(a); + return {x, y, z}; +} + +iris::Vector3 random_in_unit_sphere() +{ + std::uniform_real_distribution dist1(0.0f, 1.0f); + iris::Vector3 p; + do + { + p = + iris::Vector3{ + dist1(generator), dist1(generator), dist1(generator)} * + 2.0 - + iris::Vector3(1, 1, 1); + } while (p.dot(p) >= 1.0); + return p; +} + +/** + * Recursively trace a ray through a scene, to a max depth. + */ +iris::Colour trace(const Ray &ray, int depth) +{ + const Sphere *hit = nullptr; + auto distance = std::numeric_limits::max(); + iris::Vector3 point; + iris::Vector3 normal; + + for (const auto &shape : scene) + { + const auto &[d, p, n] = shape.intersects(ray); + + if (d < distance) + { + distance = d; + point = p; + normal = n; + hit = &shape; + } + } + + // return sky colour if we don't hit anything + if (hit == nullptr) + { + return {0.9f, 0.9f, 0.9f}; + } + + const auto emittance = hit->colour; + + // return black if we hit max depth + if (depth > 4) + { + return {0.0f, 0.0f, 0.0f}; + } + + Ray newRay{{}, {}}; + + // create another ray to trace, based on material + if (hit->is_metal) + { + auto reflect = ray.direction - normal * ray.direction.dot(normal) * 2; + newRay = { + point, + iris::Vector3::normalise( + reflect + random_in_unit_sphere() * hit->rougness)}; + newRay.origin += newRay.direction * 0.01f; + } + else + { + auto target = point + normal + random_unit_vector(); + + newRay = {point, iris::Vector3::normalise(target - point)}; + newRay.origin += newRay.direction * 0.001f; + } + + // trace next ray and mix in its colour + return emittance * trace(newRay, depth + 1); +} + +void go(int, char **) +{ + iris::Logger::instance().ignore_tag("js"); + + scene.emplace_back( + iris::Vector3{150.0f, 0.0f, -600.0f}, + 100.0f, + iris::Colour{0.58f, 0.49f, 0.67f}); + scene.emplace_back( + iris::Vector3{-150.0f, 0.0f, -600.0f}, + 100.0f, + iris::Colour{0.99f, 0.78f, 0.84f}); + scene.emplace_back( + iris::Vector3{00.0f, 0.0f, -750.0f}, + 100.0f, + iris::Colour{1.0f, 0.87f, 0.82f}); + scene.emplace_back( + iris::Vector3{0.0f, -10100.0f, -600.0f}, + 10000.0f, + iris::Colour{1.0f, 1.0f, 1.0f}); + + scene[2].is_metal = true; + scene[2].rougness = 0.9; + + static const auto width = 600; + static const auto height = 400; + std::vector pixels(width * height * 3); + std::size_t counter = 0u; + + const float fov = M_PI / 3.; + std::uniform_real_distribution dist1(-0.5f, 0.5f); + + std::vector jobs; + + LOG_INFO("job_system", "starting"); + auto start = std::chrono::high_resolution_clock::now(); + + for (std::size_t j = 0; j < height; j++) + { + for (std::size_t i = 0; i < width; i++) + { + jobs.emplace_back([i, j, fov, counter, &pixels, &dist1]() { + const auto dir_x = (i + 0.5f) - width / 2.0f; + const auto dir_y = -(j + 0.5f) + height / 2.0f; + const auto dir_z = -height / (2.0f * tan(fov / 2.0f)); + + iris::Colour pixel; + + auto samples = 100; + + for (int i = 0; i < samples; i++) + { + pixel += trace( + {{0, 0, 0}, + iris::Vector3::normalise( + {dir_x + dist1(generator), + dir_y + dist1(generator), + dir_z})}, + 1); + } + + pixel *= (1.0 / (float)samples); + + // clamp colours + pixels[counter + 0u] = static_cast( + (255.0f * std::max(0.0f, std::min(1.0f, (float)pixel.r)))); + pixels[counter + 1u] = static_cast( + (255.0f * std::max(0.0f, std::min(1.0f, (float)pixel.g)))); + pixels[counter + 2u] = static_cast( + (255.0f * std::max(0.0f, std::min(1.0f, (float)pixel.b)))); + }); + + counter += 3u; + } + } + + do + { + const auto count = std::min((std::size_t)900ul, jobs.size()); + std::vector batch(std::cend(jobs) - count, std::cend(jobs)); + jobs.erase(std::cend(jobs) - count, std::cend(jobs)); + + iris::Root::jobs_manager().wait(batch); + + } while (!jobs.empty()); + + auto end = std::chrono::high_resolution_clock::now(); + + LOG_INFO( + "job_sample", + "render time: {}ms", + std::chrono::duration_cast(end - start) + .count()); + + stbi_write_png("render.png", width, height, 3, pixels.data(), 3 * width); + + LOG_INFO("job_sample", "done"); +} + +int main(int argc, char **argv) +{ + iris::start(argc, argv, go); + + return 0; +} diff --git a/samples/networking/CMakeLists.txt b/samples/networking/CMakeLists.txt new file mode 100644 index 00000000..75d245eb --- /dev/null +++ b/samples/networking/CMakeLists.txt @@ -0,0 +1,25 @@ +set(CLIENT_SRCS client.cpp) + +set(SERVER_SRCS server.cpp) + +if(IRIS_PLATFORM MATCHES "IOS") + set(IOS_RESOURCES "Default-568h@2x.png") + + add_executable(client MACOSX_BUNDLE ${CLIENT_SRCS} ${IOS_RESOURCES}) + set(MACOSX_BUNDLE_GUI_IDENTIFIER "${IRIS_BUNDLE_IDENTIFIER}.client") + + set_target_properties( + client + PROPERTIES MACOSX_BUNDLE YES + XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "iPhone Developer" + XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "${IRIS_DEVELOPMENT_TEAM}" + RESOURCE "${IOS_RESOURCES}") + +else() + add_executable(client ${CLIENT_SRCS}) +endif() + +add_executable(server ${SERVER_SRCS}) + +target_link_libraries(client iris) +target_link_libraries(server iris) diff --git a/samples/networking/client.cpp b/samples/networking/client.cpp new file mode 100644 index 00000000..737c1b9a --- /dev/null +++ b/samples/networking/client.cpp @@ -0,0 +1,583 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/camera.h" +#include "core/data_buffer.h" +#include "core/exception.h" +#include "core/looper.h" +#include "core/quaternion.h" +#include "core/start.h" +#include "core/vector3.h" +#include "core/window.h" +#include "events/keyboard_event.h" +#include "graphics/mesh_factory.h" +#include "graphics/pipeline.h" +#include "graphics/render_entity.h" +#include "graphics/scene.h" +#include "graphics/stage.h" +#include "log/emoji_formatter.h" +#include "log/log.h" +#include "networking/client_connection_handler.h" +#include "networking/data_buffer_deserialiser.h" +#include "networking/data_buffer_serialiser.h" +#include "networking/packet.h" +#include "networking/udp_socket.h" +#include "physics/basic_character_controller.h" +#include "physics/box_collision_shape.h" +#include "physics/physics_system.h" +#include "physics/rigid_body.h" + +#include "client_input.h" + +using namespace std::chrono_literals; + +/** + * Process all pending user input. This will send input to the server as well as + * store it locally. + * + * @param inputs + * Collection to store inputs in. + * + * @param camera + * Camera to update. + * + * @param tick + * Current client tick number. + * + * @param client + * Object to communicate with server. + * + * @returns + * True if user has quit, false otherwise. + */ +bool handle_input( + std::deque &inputs, + iris::Camera &camera, + std::uint32_t tick, + iris::ClientConnectionHandler &client, + iris::Window &window) +{ + auto quit = false; + + // we just continuously update a static input object, this ensures the + // object always represents the current input state + static ClientInput input; + + auto has_input = false; + + // consume all inputs + for (;;) + { + auto evt = window.pump_event(); + if (!evt) + { + break; + } + + if (evt->is_key(iris::Key::ESCAPE)) + { + quit = true; + } + else if (evt->is_key()) + { + const auto keyboard = evt->key(); + + // convert input keys to client input + switch (keyboard.key) + { + case iris::Key::W: + input.forward = + keyboard.state == iris::KeyState::DOWN ? -1.0f : 0.0f; + break; + case iris::Key::S: + input.forward = + keyboard.state == iris::KeyState::DOWN ? 1.0f : 0.0f; + break; + case iris::Key::A: + input.side = + keyboard.state == iris::KeyState::DOWN ? -1.0f : 0.0f; + break; + case iris::Key::D: + input.side = + keyboard.state == iris::KeyState::DOWN ? 1.0f : 0.0f; + break; + default: break; + } + + has_input = true; + } + else if (evt->is_mouse()) + { + // update camera based on mouse movement + static const auto sensitivity = 0.0025f; + const auto mouse = evt->mouse(); + + camera.adjust_yaw(mouse.delta_x * sensitivity); + camera.adjust_pitch(-mouse.delta_y * sensitivity); + } + } + + // if we processed any input then send the latest input state to the server + // and store a copy locally + if (has_input) + { + input.tick = tick; + + iris::DataBufferSerialiser serialiser; + input.serialise(serialiser); + client.send(serialiser.data(), iris::ChannelType::RELIABLE_ORDERED); + + inputs.emplace_back(input); + } + + return quit; +} + +/** + * Process an update from the server on the state of the world. + * + * @param server_data + * Data from server + * + * @param snapshots + * Collection snapshots of server updates (of non player entity). + * + * @param history + * Collection of local state history. + * + * @param inputs + * Collection of stored user inputs. + * + * @returns + * Tuple of various server data. + */ +std::tuple +process_server_update( + const iris::DataBuffer &server_data, + std::deque> &snapshots, + std::vector>> + &history, + std::deque &inputs) +{ + // deserialise server update + iris::DataBufferDeserialiser deserialiser(server_data); + const auto position = deserialiser.pop(); + const auto linear_velocity = deserialiser.pop(); + const auto angular_velocity = deserialiser.pop(); + const auto last_acked = deserialiser.pop(); + const auto box_pos = deserialiser.pop(); + auto box_rot = deserialiser.pop(); + box_rot.normalise(); + + // store server update of box, put the time in the future so we can easily + // interpolate between snapshots + snapshots.emplace_back( + std::chrono::steady_clock::now() + 100ms, box_pos, box_rot); + + // find the last input acknowledged by the server + const auto acked_input = std::find_if( + std::cbegin(inputs), + std::cend(inputs), + [last_acked](const ClientInput &element) { + return element.tick >= last_acked; + }); + + // cleanup old inputs + if (acked_input != std::cend(inputs)) + { + inputs.erase(std::cbegin(inputs), acked_input); + } + + // find last history entry acknowledged by the server + const auto acked_history = std::find_if( + std::cbegin(history), + std::cend(history), + [last_acked](const auto &element) { + return std::get<0>(element) == last_acked; + }); + + // cleanup old history + if (acked_history != std::cend(history)) + { + history.erase(std::cbegin(history), acked_history); + } + + // return useful data + return {last_acked, position, linear_velocity, angular_velocity}; +} + +/** + * Update the client prediction based on server updates. This will compare the + * actual server position at a given tick to our historical prediction at that + * time. If they differ by more than some threshold we will: + * - reset to that state + * - apply the correct server details + * - replay all user inputs since that tick + * + * @param last_acked + * The last tick the server acknowledged. + * + * @param history + * Collection of local state history. + * + * @param server_position + * The true position of the client at last_acked. + * + * @param linear_velocity + * The true linear_velocity of the client at last_acked. + * + * @param angular_velocity + * The true angular_velocity of the client at last_acked. + * + * @param character_controller + * Pointer to character controller. + * + * @param inputs + * Collection of stored user inputs. + * + * @param physics_system + * Physics system. + */ +void client_prediciton( + std::uint32_t last_acked, + std::vector>> + &history, + const iris::Vector3 &server_position, + const iris::Vector3 &linear_velocity, + const iris::Vector3 &angular_velocity, + iris::CharacterController *character_controller, + std::deque &inputs, + iris::PhysicsSystem *physics_system) +{ + const auto &[tck_num, pos, state] = history.front(); + + // as we periodically purge acked history if we have an entry for the last + // acked tick it will be the first in our collection + if (tck_num == last_acked) + { + // get the difference between our saved predicted position and the + // actual server prediction + static const auto threshold = 0.3f; + const auto diff = std::abs((pos - server_position).magnitude()); + + // if the difference is above our threshold (which account for floating + // point rounding errors) then we have gone out of sync with the server + if (diff >= threshold) + { + // reset to the stored state + physics_system->load(state.get()); + + // update the player with the server supplied data + character_controller->reposition( + server_position, iris::Quaternion{0.0f, 1.0f, 0.0f, 0.0f}); + character_controller->set_linear_velocity(linear_velocity); + character_controller->set_angular_velocity(angular_velocity); + + // update the history entry + history[0] = std::make_tuple( + tck_num, + character_controller->position(), + physics_system->save()); + + // update every other history entry by replaying user inputs + for (auto i = 1u; i < history.size(); ++i) + { + auto current_tick = tck_num + i; + + // replay user inputs for our current history entry + for (const auto &input : inputs) + { + if (input.tick == current_tick) + { + iris::Vector3 walk_direction{ + input.side, 0.0f, input.forward}; + walk_direction.normalise(); + character_controller->set_walk_direction( + walk_direction); + } + } + + // step physics + physics_system->step(std::chrono::milliseconds(33)); + + // update history + history[i] = std::make_tuple( + current_tick, + character_controller->position(), + physics_system->save()); + } + } + } + + // we should now be back in sync with the server +} + +/** + * Interpolate updates from the server. As the server send updates at fixed + * intervals rendering each update would give jerky motion. Instead we delay + * rendering by a small amount so we have two snapshots of the entity positions, + * we can then interpolate between them for smoother motion. + * + * @param snapshots + * Collection snapshots of server updates (of non player entity). + * + * @param box + * Pointer to RenderEntity for box. + */ +void entity_interpolation( + std::deque> &snapshots, + iris::RenderEntity *box) +{ + const auto now = std::chrono::steady_clock::now(); + + // find the first snapshot that is ahead of us in time, this will be the + // snapshot we interpolate towards + const auto second_snapshot = std::find_if( + std::cbegin(snapshots) + 1u, + std::cend(snapshots), + [&now](const auto &element) { return std::get<0>(element) >= now; }); + + // check we have two snapshots + if (second_snapshot != std::cend(snapshots)) + { + const auto first_snapshot = second_snapshot - 1u; + + auto [time_start, pos_start, rot_start] = *first_snapshot; + const auto &[time_end, pos_end, rot_end] = *second_snapshot; + + // calculate the interpolate amount + const auto delta1 = now - time_start; + const auto delta2 = time_end - time_start; + const auto lerp_amount = static_cast(delta1.count()) / + static_cast(delta2.count()); + + pos_start.lerp(pos_end, lerp_amount); + rot_start.slerp(rot_end, lerp_amount); + + box->set_position(pos_start); + box->set_orientation(rot_start); + + // purge old snapshots + snapshots.erase(std::cbegin(snapshots), first_snapshot); + } +} + +void go(int, char **) +{ + LOG_DEBUG("client", "hello world"); + + auto socket = std::make_unique("127.0.0.1", 8888); + + iris::ClientConnectionHandler client{std::move(socket)}; + + iris::Window window{800u, 800u}; + iris::Camera camera{iris::CameraType::PERSPECTIVE, 800u, 800u}; + + auto scene = std::make_unique(); + + scene->create_entity( + nullptr, + iris::mesh_factory::cube({1.0f, 1.0f, 1.0f}), + iris::Transform{ + iris::Vector3{0.0f, -50.0f, 0.0f}, + {}, + iris::Vector3{500.0f, 50.0f, 500.0f}}); + + auto *box = scene->create_entity( + nullptr, + iris::mesh_factory::cube({1.0f, 0.0f, 0.0f}), + iris::Transform{ + iris::Vector3{0.0f, 1.0f, 0.0f}, + {}, + iris::Vector3{0.5f, 0.5f, 0.5f}}); + + iris::Pipeline pipeline{}; + pipeline.add_stage(std::move(scene), camera); + + std::deque> + snapshots; + std::deque inputs; + + iris::PhysicsSystem ps{}; + auto *character_controller = + ps.create_character_controller(&ps); + ps.create_rigid_body( + iris::Vector3{0.0f, -50.0f, 0.0f}, + std::make_unique( + iris::Vector3{500.0f, 50.0f, 500.0f}), + iris::RigidBodyType::STATIC); + + std::vector>> + history; + std::uint32_t tick = 0u; + + ClientInput input; + + // keep looping till handshake and sync is complete which will give us a lag + // estimate + while (client.lag().count() == 0) + { + client.flush(); + } + + LOG_WARN("client", "lag: {}", client.lag().count()); + + // calculate how many ticks we are behind the server based on lag + const auto ticks_behind = (client.lag().count() / 33u) + 20u; + LOG_INFO("client", "ticks behind {}", ticks_behind); + + // step ourselves forward in time + // we want be a head of the server just enough so that it received input for + // the frame it is about to execute + for (auto i = 0u; i < ticks_behind; ++i) + { + ps.step(std::chrono::milliseconds(33)); + } + + tick = ticks_behind; + + iris::Looper looper( + 33ms * ticks_behind, + 33ms, + [&](std::chrono::microseconds, std::chrono::microseconds) { + // fixed timestep function + // this simulates the same physics code as in the server, with the + // same inputs + + auto keep_looping = false; + + // process user inputs + if (!handle_input(inputs, camera, tick, client, window)) + { + // apply inputs to physics simulation + for (const auto &input : inputs) + { + // only process inputs for this tick + if (input.tick == tick) + { + iris::Vector3 walk_direction{ + input.side, 0.0f, input.forward}; + walk_direction.normalise(); + + character_controller->set_walk_direction( + walk_direction); + } + } + + ps.step(33ms); + + // snapshot the state of the simulation at this tick, this is + // needed so we can rewind the state of we get out of sync with + // the server + history.emplace_back( + tick, character_controller->position(), ps.save()); + + ++tick; + keep_looping = true; + } + + return keep_looping; + }, + [&](std::chrono::microseconds, std::chrono::microseconds) { + // variable timestep function + // this handles various lag compensation techniques as well as + // renders the world + + // process all pending messages from the server + for (;;) + { + const auto server_data = + client.try_read(iris::ChannelType::RELIABLE_ORDERED); + if (!server_data) + { + break; + } + + // process the message + const auto + [last_acked, + server_position, + linear_velocity, + angular_velocity] = + process_server_update( + *server_data, snapshots, history, inputs); + + // if we have history then update the client prediction based + // upon the latest information from the server + if (!history.empty()) + { + client_prediciton( + last_acked, + history, + server_position, + linear_velocity, + angular_velocity, + character_controller, + inputs, + &ps); + } + } + + // put the camera where the player is + camera.set_position(character_controller->position()); + + // if we have snapshots then interpolate the server entity for + // smooth motion + if (snapshots.size() >= 2u) + { + entity_interpolation(snapshots, box); + } + + // render the world + window.render(pipeline); + + return true; + }); + + looper.run(); + + LOG_ERROR("client", "goodbye!"); +} + +int main(int argc, char **argv) +{ + iris::start_debug(argc, argv, go); + + return 0; +} diff --git a/samples/networking/client_input.h b/samples/networking/client_input.h new file mode 100644 index 00000000..71e6b9c8 --- /dev/null +++ b/samples/networking/client_input.h @@ -0,0 +1,65 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "core/vector3.h" +#include "networking/data_buffer_deserialiser.h" +#include "networking/data_buffer_serialiser.h" +#include + +/** + * Struct encapsulating user input. + */ +struct ClientInput +{ + /** + * Create new ClientInput. + */ + ClientInput() + : forward(0.0f) + , side(0.0f) + , tick(0u) + { + } + + /** + * Create new ClientInput with a deserialiser. + * + * @param deserialiser + * Deserialiser object. + */ + ClientInput(iris::DataBufferDeserialiser &deserialiser) + : forward(deserialiser.pop()) + , side(deserialiser.pop()) + , tick(deserialiser.pop()) + { + } + + /** + * Serialise object. + * + * @param serialiser. + * Serialiser object. + */ + void serialise(iris::DataBufferSerialiser &serialiser) const + { + serialiser.push(forward); + serialiser.push(side); + serialiser.push(tick); + } + + /** Relative amount user is moving forward, should be in range [-1.0, 1.0]. + */ + float forward; + + /** Relative amount user is moving sideways, should be in range [-1.0, 1.0]. + */ + float side; + + /** Client tick input is for. */ + std::uint32_t tick; +}; diff --git a/samples/networking/server.cpp b/samples/networking/server.cpp new file mode 100644 index 00000000..4871a9bd --- /dev/null +++ b/samples/networking/server.cpp @@ -0,0 +1,179 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/data_buffer.h" +#include "core/exception.h" +#include "core/looper.h" +#include "core/vector3.h" +#include "log/log.h" +#include "networking/data_buffer_deserialiser.h" +#include "networking/data_buffer_serialiser.h" +#include "networking/networking.h" +#include "networking/packet.h" +#include "networking/server_connection_handler.h" +#include "networking/udp_server_socket.h" +#include "physics/basic_character_controller.h" +#include "physics/box_collision_shape.h" +#include "physics/physics_system.h" +#include "physics/rigid_body.h" +#include "events/keyboard_event.h" + +#include "client_input.h" + +using namespace std::chrono_literals; + +iris::CharacterController *character_controller = nullptr; +std::size_t player_id = std::numeric_limits::max(); + +void go(int, char **) +{ + iris::Logger::instance().set_log_engine(true); + + LOG_DEBUG("server_sample", "hello world"); + + std::deque inputs; + auto tick = 0u; + + auto socket = std::make_unique("127.0.0.1", 8888); + + iris::ServerConnectionHandler connection_handler( + std::move(socket), + [](std::size_t id) { + LOG_DEBUG("server", "new connection {}", id); + + // just support a single player + player_id = id; + }, + [&inputs, &tick]( + std::size_t id, + const iris::DataBuffer &data, + iris::ChannelType type) { + if (type == iris::ChannelType::RELIABLE_ORDERED) + { + iris::DataBufferDeserialiser deserialiser(data); + ClientInput input{deserialiser}; + + if (input.tick >= tick) + { + // if input is for now or the future (which it should be as + // the client runs ahead) then store it + inputs.emplace_back(input); + } + else + { + LOG_WARN("server", "stale input: {} {}", tick, input.tick); + } + } + }); + + iris::PhysicsSystem ps{}; + character_controller = + ps.create_character_controller(&ps); + ps.create_rigid_body( + iris::Vector3{0.0f, -50.0f, 0.0f}, + std::make_unique( + iris::Vector3{500.0f, 50.0f, 500.0f}), + iris::RigidBodyType::STATIC); + auto *box = ps.create_rigid_body( + iris::Vector3{0.0f, 1.0f, 0.0f}, + std::make_unique( + iris::Vector3{0.5f, 0.5f, 0.5f}), + iris::RigidBodyType::NORMAL); + + // block and wait for client to connect + while (player_id == std::numeric_limits::max()) + { + connection_handler.update(); + std::this_thread::sleep_for(100ms); + } + + std::chrono::microseconds step(0); + + ps.step(33ms); + + iris::Looper looper{ + 0ms, + 33ms, + [&](std::chrono::microseconds clock, + std::chrono::microseconds time_step) { + // fixed timestep function + // this runs the physics and processes player input + + for (const auto &input : inputs) + { + // if stored input is for our current tick then apply it to + // the physics simulation + if (input.tick == tick) + { + iris::Vector3 walk_direction{ + input.side, 0.0f, input.forward}; + walk_direction.normalise(); + + character_controller->set_walk_direction(walk_direction); + } + if (input.tick > tick) + { + break; + } + } + + ps.step(33ms); + ++tick; + + return true; + }, + [&](std::chrono::microseconds clock, + std::chrono::microseconds time_step) { + // variable timestep function + // sends snapshots of the world to the client + + connection_handler.update(); + + // whilst this is a variable time function we only want to send out + // updates every 100ms + if (clock > step + 100ms) + { + // serialise world state + iris::DataBufferSerialiser serialiser; + serialiser.push(character_controller->position()); + serialiser.push(character_controller->linear_velocity()); + serialiser.push(character_controller->angular_velocity()); + serialiser.push(tick); + serialiser.push(box->position()); + serialiser.push(box->orientation()); + + connection_handler.send( + player_id, + serialiser.data(), + iris::ChannelType::RELIABLE_ORDERED); + + step = clock; + } + + return true; + }}; + + looper.run(); +} + +int main(int argc, char **argv) +{ + go(argc, argv); + + return 0; +} diff --git a/samples/sample_browser/CMakeLists.txt b/samples/sample_browser/CMakeLists.txt new file mode 100644 index 00000000..dbe27ebb --- /dev/null +++ b/samples/sample_browser/CMakeLists.txt @@ -0,0 +1,21 @@ +add_executable(sample_browser + main.cpp + samples/animation_sample.cpp + samples/animation_sample.h + samples/physics_sample.cpp + samples/physics_sample.h + samples/render_graph_sample.cpp + samples/render_graph_sample.h + samples/sample.h) + +file(COPY assets DESTINATION ${PROJECT_BINARY_DIR}/samples/sample_browser) + +target_include_directories(sample_browser + PRIVATE ${stb_SOURCE_DIR}) +target_include_directories( + sample_browser PUBLIC "${PROJECT_SOURCE_DIR}/samples/sample_browser") +target_link_libraries(sample_browser iris) + +if(IRIS_PLATFORM MATCHES "WIN32") + set_target_properties(sample_browser PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDebug") +endif() diff --git a/samples/sample_browser/assets/Zombie.fbx b/samples/sample_browser/assets/Zombie.fbx new file mode 100644 index 00000000..d984ad2a Binary files /dev/null and b/samples/sample_browser/assets/Zombie.fbx differ diff --git a/samples/sample_browser/assets/ZombieTexture.png b/samples/sample_browser/assets/ZombieTexture.png new file mode 100644 index 00000000..85579c9f Binary files /dev/null and b/samples/sample_browser/assets/ZombieTexture.png differ diff --git a/samples/sample_browser/assets/brickwall.jpg b/samples/sample_browser/assets/brickwall.jpg new file mode 100644 index 00000000..39478d3e Binary files /dev/null and b/samples/sample_browser/assets/brickwall.jpg differ diff --git a/samples/sample_browser/assets/brickwall_normal.jpg b/samples/sample_browser/assets/brickwall_normal.jpg new file mode 100644 index 00000000..a0a5fb62 Binary files /dev/null and b/samples/sample_browser/assets/brickwall_normal.jpg differ diff --git a/samples/sample_browser/assets/circle.png b/samples/sample_browser/assets/circle.png new file mode 100644 index 00000000..628cf69c Binary files /dev/null and b/samples/sample_browser/assets/circle.png differ diff --git a/samples/sample_browser/assets/crate.png b/samples/sample_browser/assets/crate.png new file mode 100644 index 00000000..596e8da3 Binary files /dev/null and b/samples/sample_browser/assets/crate.png differ diff --git a/samples/sample_browser/assets/crate_specular.png b/samples/sample_browser/assets/crate_specular.png new file mode 100644 index 00000000..681bf6ef Binary files /dev/null and b/samples/sample_browser/assets/crate_specular.png differ diff --git a/samples/sample_browser/assets/sphere.fbx b/samples/sample_browser/assets/sphere.fbx new file mode 100644 index 00000000..54d8146f Binary files /dev/null and b/samples/sample_browser/assets/sphere.fbx differ diff --git a/samples/sample_browser/main.cpp b/samples/sample_browser/main.cpp new file mode 100644 index 00000000..2f843d8e --- /dev/null +++ b/samples/sample_browser/main.cpp @@ -0,0 +1,152 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include + +#include "samples/animation_sample.h" +#include "samples/physics_sample.h" +#include "samples/render_graph_sample.h" +#include "samples/sample.h" + +static constexpr std::size_t sample_count = 3u; + +using namespace std::chrono_literals; + +std::unique_ptr create_sample( + iris::Window *window, + iris::RenderTarget *screen_target, + std::size_t index) +{ + std::unique_ptr sample{}; + + switch (index) + { + case 0: + sample = std::make_unique(window, screen_target); + break; + case 1: + sample = std::make_unique(window, screen_target); + break; + case 2: + sample = std::make_unique(window, screen_target); + break; + default: throw iris::Exception("unknown sample index"); + } + + return sample; +} + +void go(int, char **) +{ + iris::ResourceLoader::instance().set_root_directory("assets"); + + auto window = iris::Root::window_manager().create_window(800u, 800u); + std::size_t sample_number = 0u; + + auto *target = window->create_render_target(); + iris::Camera camera{ + iris::CameraType::ORTHOGRAPHIC, window->width(), window->height()}; + iris::RenderEntity *title = nullptr; + iris::RenderEntity *fps = nullptr; + + auto scene = std::make_unique(); + auto *rg = scene->create_render_graph(); + rg->render_node()->set_colour_input( + rg->create(target->colour_texture())); + scene->create_entity( + rg, + iris::Root::mesh_manager().sprite({1.0f, 1.0f, 1.0f}), + iris::Transform{{0.0f}, {}, {800.0f, 800.0f, 1.0f}}); + + auto sample = create_sample(window, target, sample_number % sample_count); + + iris::RenderPass pass{scene.get(), &camera, nullptr}; + + auto passes = sample->render_passes(); + passes.emplace_back(pass); + + window->set_render_passes(passes); + + std::size_t frame_counter = 0u; + std::size_t next_update = 1u; + + iris::Looper looper{ + 0ms, + 16ms, + [&sample](auto, auto) { + sample->fixed_update(); + return true; + }, + [&sample, + &window, + &sample_number, + &target, + &title, + &fps, + &pass, + &frame_counter, + &next_update](std::chrono::microseconds elapsed, auto) { + auto running = true; + auto event = window->pump_event(); + while (event) + { + if (event->is_quit() || event->is_key(iris::Key::ESCAPE)) + { + running = false; + } + else if (event->is_key(iris::Key::TAB, iris::KeyState::UP)) + { + ++sample_number; + sample = create_sample( + window, target, sample_number % sample_count); + + auto passes = sample->render_passes(); + passes.emplace_back(pass); + + window->set_render_passes(passes); + } + else + { + sample->handle_input(*event); + } + event = window->pump_event(); + } + + sample->variable_update(); + window->render(); + + return running; + }}; + + looper.run(); +} + +int main(int argc, char **argv) +{ + iris::start(argc, argv, go); +} \ No newline at end of file diff --git a/samples/sample_browser/samples/animation_sample.cpp b/samples/sample_browser/samples/animation_sample.cpp new file mode 100644 index 00000000..0951ffb4 --- /dev/null +++ b/samples/sample_browser/samples/animation_sample.cpp @@ -0,0 +1,301 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "animation_sample.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +/** + * Helper function to update camera based on user input. + * + * @param camera + * Camera to update. + * + * @param key_map + * Map of user pressed keys. + */ +void update_camera( + iris::Camera &camera, + const std::map &key_map) +{ + static auto speed = 2.0f; + iris::Vector3 velocity; + + if (key_map.at(iris::Key::W) == iris::KeyState::DOWN) + { + velocity += camera.direction() * speed; + } + + if (key_map.at(iris::Key::S) == iris::KeyState::DOWN) + { + velocity -= camera.direction() * speed; + } + + if (key_map.at(iris::Key::A) == iris::KeyState::DOWN) + { + velocity -= camera.right() * speed; + } + + if (key_map.at(iris::Key::D) == iris::KeyState::DOWN) + { + velocity += camera.right() * speed; + } + + if (key_map.at(iris::Key::Q) == iris::KeyState::DOWN) + { + velocity += camera.right().cross(camera.direction()) * speed; + } + + if (key_map.at(iris::Key::E) == iris::KeyState::DOWN) + { + velocity -= camera.right().cross(camera.direction()) * speed; + } + + camera.translate(velocity); +} + +} + +AnimationSample::AnimationSample( + iris::Window *window, + iris::RenderTarget *target) + : window_(window) + , target_(target) + , scene_() + , light_transform_() + , light_(nullptr) + , camera_( + iris::CameraType::PERSPECTIVE, + window_->width(), + window_->height()) + , physics_(iris::Root::physics_manager().create_physics_system()) + , zombie_(nullptr) + , animation_(0u) + , animations_() + , hit_boxes_() + , hit_box_data_() + , key_map_() +{ + key_map_ = { + {iris::Key::W, iris::KeyState::UP}, + {iris::Key::A, iris::KeyState::UP}, + {iris::Key::S, iris::KeyState::UP}, + {iris::Key::D, iris::KeyState::UP}, + {iris::Key::Q, iris::KeyState::UP}, + {iris::Key::E, iris::KeyState::UP}, + }; + + camera_.set_position(camera_.position() + iris::Vector3{0.0f, 5.0f, 0.0f}); + + scene_.set_ambient_light({0.1f, 0.1f, 0.1f, 1.0f}); + + auto *floor_graph = scene_.create_render_graph(); + + floor_graph->render_node()->set_specular_amount_input( + floor_graph->create>(0.0f)); + + auto &mesh_manager = iris::Root::mesh_manager(); + + scene_.create_entity( + floor_graph, + mesh_manager.cube({1.0f, 1.0f, 1.0f}), + iris::Transform{ + iris::Vector3{0.0f, -500.0f, 0.0f}, {}, iris::Vector3{500.0f}}); + + auto *render_graph = scene_.create_render_graph(); + + auto *texture = + render_graph->create("ZombieTexture.png"); + + render_graph->render_node()->set_colour_input(texture); + + zombie_ = scene_.create_entity( + render_graph, + mesh_manager.load_mesh("Zombie.fbx"), + iris::Transform{ + iris::Vector3{0.0f, 0.0f, 0.0f}, {}, iris::Vector3{0.05f}}, + mesh_manager.load_skeleton("Zombie.fbx")); + + zombie_->set_receive_shadow(false); + + auto *debug_draw = scene_.create_entity( + nullptr, + mesh_manager.cube({}), + iris::Vector3{}, + iris::PrimitiveType::LINES); + + light_ = scene_.create_light( + iris::Vector3{-1.0f, -1.0f, 0.0f}, true); + light_transform_ = iris::Transform{light_->direction(), {}, {1.0f}}; + + physics_->enable_debug_draw(debug_draw); + + animations_ = { + "Zombie|ZombieWalk", + "Zombie|ZombieBite", + "Zombie|ZombieCrawl", + "Zombie|ZombieIdle", + "Zombie|ZombieRun"}; + + zombie_->skeleton().set_animation(animations_[animation_]); + + // offsets and scales for bones we want to add rigid bodies to, these were + // all handcrafted + hit_box_data_ = { + {"Head", {{}, {1.0f}}}, + {"HeadTop_End", {{0.0f, -0.2f, 0.0f}, {1.0f}}}, + + {"RightArm", {{}, {1.0f}}}, + {"RightForeArm", {{}, {1.0f, 2.5f, 1.0f}}}, + {"RightHand", {{}, {1.0f}}}, + + {"LeftArm", {{}, {1.0f}}}, + {"LeftForeArm", {{}, {1.0f, 2.5f, 1.0f}}}, + {"LeftHand", {{}, {1.0f}}}, + + {"Spine", {{}, {2.0f, 1.0f, 1.0f}}}, + {"Spine1", {{}, {2.0f, 1.0f, 1.0f}}}, + {"Spine2", {{}, {2.0f, 1.0f, 1.0f}}}, + {"Hips", {{}, {1.0f}}}, + + {"LeftUpLeg", {{0.0f, 0.6f, 0.0f}, {1.0f, 2.5f, 1.0f}}}, + {"LeftLeg", {{0.0f, 0.6f, 0.0f}, {1.0f, 3.0f, 1.0f}}}, + {"LeftFoot", {{}, {1.0f, 2.5f, 1.0f}}}, + + {"RightUpLeg", {{0.0f, 0.6f, 0.0f}, {1.0f, 2.5f, 1.0f}}}, + {"RightLeg", {{0.0f, 0.6f, 0.0f}, {1.0f, 3.0f, 1.0f}}}, + {"RightFoot", {{}, {1.0f, 2.5f, 1.0f}}}}; + + // iterate all bones and create hit boxes for those we have data for + for (auto i = 0u; i < zombie_->skeleton().bones().size(); ++i) + { + const auto &bone = zombie_->skeleton().bone(i); + + const auto box_data = hit_box_data_.find(bone.name()); + if (box_data != std::end(hit_box_data_)) + { + const auto &[pos_offset, scale_offset] = box_data->second; + + // get bone transform in world space + const auto transform = iris::Transform{ + zombie_->transform() * zombie_->skeleton().transforms()[i] * + iris::Matrix4::invert(bone.offset())}; + + // create hit box + hit_boxes_[box_data->first] = { + i, + physics_->create_rigid_body( + iris::Vector3{}, + physics_->create_box_collision_shape(scale_offset), + iris::RigidBodyType::GHOST)}; + + // calculate hit box location after offset is applied + const auto offset = + transform * iris::Matrix4::make_translate(pos_offset); + + // update hit box + std::get<1>(hit_boxes_[box_data->first]) + ->reposition(offset.translation(), transform.rotation()); + std::get<1>(hit_boxes_[box_data->first])->set_name(box_data->first); + } + } + + fixed_update(); +} + +void AnimationSample::fixed_update() +{ + update_camera(camera_, key_map_); + + light_transform_.set_matrix( + iris::Matrix4(iris::Quaternion{{0.0f, 1.0f, 0.0f}, -0.01f}) * + light_transform_.matrix()); + light_->set_direction(light_transform_.translation()); + + physics_->step(std::chrono::milliseconds(13)); +} + +void AnimationSample::variable_update() +{ + zombie_->skeleton().advance(); + + // update hit boxes + for (auto &[name, data] : hit_boxes_) + { + auto &[index, body] = data; + + // get hotbox transform in world space + const auto transform = iris::Transform{ + zombie_->transform() * zombie_->skeleton().transforms()[index] * + iris::Matrix4::invert(zombie_->skeleton().bone(index).offset())}; + const auto offset = transform * iris::Matrix4::make_translate( + std::get<0>(hit_box_data_[name])); + + body->reposition(offset.translation(), transform.rotation()); + } +} + +void AnimationSample::handle_input(const iris::Event &event) +{ + if (event.is_key()) + { + const auto keyboard = event.key(); + key_map_[keyboard.key] = keyboard.state; + + if ((keyboard.key == iris::Key::SPACE) && + (keyboard.state == iris::KeyState::UP)) + { + animation_ = (animation_ + 1u) % animations_.size(); + zombie_->skeleton().set_animation(animations_[animation_]); + } + } + else if (event.is_mouse()) + { + static const auto sensitivity = 0.0025f; + const auto mouse = event.mouse(); + + camera_.adjust_yaw(mouse.delta_x * sensitivity); + camera_.adjust_pitch(-mouse.delta_y * sensitivity); + } +} + +std::vector AnimationSample::render_passes() +{ + return {{&scene_, &camera_, target_}}; +} + +std::string AnimationSample::title() const +{ + return "Animation"; +} diff --git a/samples/sample_browser/samples/animation_sample.h b/samples/sample_browser/samples/animation_sample.h new file mode 100644 index 00000000..015a82e5 --- /dev/null +++ b/samples/sample_browser/samples/animation_sample.h @@ -0,0 +1,111 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sample.h" + +/** + * Sample showcasing animation. + */ +class AnimationSample : public Sample +{ + public: + /** + * Create a new AnimationSample. + * + * @param window + * Window to render with. + * + * @param target + * Target to render to. + */ + AnimationSample(iris::Window *window, iris::RenderTarget *target); + ~AnimationSample() override = default; + + /** + * Fixed rate update function. + */ + void fixed_update() override; + + /** + * Variable rate update function. + */ + void variable_update() override; + + /** + * Handle a user input. + * + * @param event + * User input event. + */ + void handle_input(const iris::Event &event) override; + + std::vector render_passes() override; + + /** + * Title of sample. + * + * @returns + * Sample title. + */ + std::string title() const override; + + private: + /** Pointer to window. */ + iris::Window *window_; + + iris::RenderTarget *target_; + + iris::Scene scene_; + + /** Transform for moving light. */ + iris::Transform light_transform_; + + /** Scene light */ + iris::DirectionalLight *light_; + + /** Render camera. */ + iris::Camera camera_; + + /** Physics system */ + iris::PhysicsSystem *physics_; + + /** Zombie entity. */ + iris::RenderEntity *zombie_; + + /** Current animation number. */ + std::size_t animation_; + + /** Collection of animation names. */ + std::vector animations_; + + /** Mapping of bone name to index and rigid body. */ + std::map> + hit_boxes_; + + /** Mapping of bone name to offsets. */ + std::map> + hit_box_data_; + + /** User input key map. */ + std::map key_map_; +}; diff --git a/samples/sample_browser/samples/physics_sample.cpp b/samples/sample_browser/samples/physics_sample.cpp new file mode 100644 index 00000000..a0c6cef0 --- /dev/null +++ b/samples/sample_browser/samples/physics_sample.cpp @@ -0,0 +1,235 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "physics_sample.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +namespace +{ + +/** + * Helper function to update camera based on user input. + * + * @param camera + * Camera to update. + * + * @param character_controller + * Player character controller. + * + * @param key_map + * Map of user pressed keys. + */ +void update_camera( + iris::Camera &camera, + iris::CharacterController *character_controller, + const std::map &key_map) +{ + iris::Vector3 walk_direction{}; + + if (key_map.at(iris::Key::W) == iris::KeyState::DOWN) + { + walk_direction += camera.direction(); + } + + if (key_map.at(iris::Key::S) == iris::KeyState::DOWN) + { + walk_direction -= camera.direction(); + } + + if (key_map.at(iris::Key::A) == iris::KeyState::DOWN) + { + walk_direction -= camera.right(); + } + + if (key_map.at(iris::Key::D) == iris::KeyState::DOWN) + { + walk_direction += camera.right(); + } + + if (key_map.at(iris::Key::SPACE) == iris::KeyState::DOWN) + { + character_controller->jump(); + } + + walk_direction.normalise(); + character_controller->set_walk_direction(walk_direction); + + const auto cam_pos = character_controller->position(); + + camera.translate( + cam_pos - camera.position() + iris::Vector3{0.0f, 0.5f, 0.0f}); +} + +} + +PhysicsSample::PhysicsSample(iris::Window *window, iris::RenderTarget *target) + : window_(window) + , target_(target) + , scene_() + , physics_(iris::Root::physics_manager().create_physics_system()) + , light_transform_() + , light_(nullptr) + , camera_( + iris::CameraType::PERSPECTIVE, + window_->width(), + window_->height()) + , key_map_() + , boxes_() + , character_controller_(nullptr) +{ + key_map_ = { + {iris::Key::W, iris::KeyState::UP}, + {iris::Key::A, iris::KeyState::UP}, + {iris::Key::S, iris::KeyState::UP}, + {iris::Key::D, iris::KeyState::UP}, + {iris::Key::Q, iris::KeyState::UP}, + {iris::Key::E, iris::KeyState::UP}, + {iris::Key::SPACE, iris::KeyState::UP}, + }; + + camera_.set_position(camera_.position() + iris::Vector3{0.0f, 5.0f, 0.0f}); + + scene_.set_ambient_light({0.1f, 0.1f, 0.1f}); + scene_.create_light( + iris::Vector3{0.0f, -1.0f, -1.0f}, true); + light_ = scene_.create_light( + iris::Vector3{0.0f, 1.0f, -10.0f}); + + auto *floor_graph = scene_.create_render_graph(); + floor_graph->render_node()->set_specular_amount_input( + floor_graph->create>(0.0f)); + + auto &mesh_manager = iris::Root::mesh_manager(); + + scene_.create_entity( + floor_graph, + mesh_manager.cube({1.0f, 1.0f, 1.0f}), + iris::Transform{ + iris::Vector3{0.0f, -50.0f, 0.0f}, + {}, + iris::Vector3{500.0f, 50.0f, 500.0f}}); + + auto *box_graph = scene_.create_render_graph(); + box_graph->render_node()->set_colour_input( + box_graph->create("crate.png")); + box_graph->render_node()->set_specular_amount_input( + box_graph->create( + box_graph->create("crate_specular.png"), "r")); + + auto width = 10u; + auto height = 5u; + + for (auto y = 0u; y < height; ++y) + { + for (auto x = 0u; x < width; ++x) + { + const iris::Vector3 pos{ + static_cast(x), static_cast(y + 0.5f), 0.0f}; + static const iris::Vector3 half_size{0.5f, 0.5f, 0.5f}; + auto colour = ((y * height) + x + (y % 2)) % 2 == 0 + ? iris::Colour{1.0f, 0.0f, 0.0f} + : iris::Colour{0.0f, 0.0f, 1.0f}; + + boxes_.emplace_back( + scene_.create_entity( + box_graph, + mesh_manager.cube({1.0f, 1.0f, 1.0f}), + iris::Transform{pos, {}, half_size}), + physics_->create_rigid_body( + pos, + physics_->create_box_collision_shape(half_size), + iris::RigidBodyType::NORMAL)); + } + } + + light_transform_ = iris::Transform{light_->position(), {}, {1.0f}}; + + character_controller_ = physics_->create_character_controller(); + + physics_->create_rigid_body( + iris::Vector3{0.0f, -50.0f, 0.0f}, + physics_->create_box_collision_shape({500.0f, 50.0f, 500.0f}), + iris::RigidBodyType::STATIC); +} + +void PhysicsSample::fixed_update() +{ + update_camera(camera_, character_controller_, key_map_); + + light_transform_.set_matrix( + iris::Matrix4(iris::Quaternion{{0.0f, 1.0f, 0.0f}, -0.01f}) * + light_transform_.matrix()); + light_->set_position(light_transform_.translation()); + + physics_->step(16ms); +} + +void PhysicsSample::variable_update() +{ + const auto cam_pos = character_controller_->position(); + + camera_.translate( + cam_pos - camera_.position() + iris::Vector3{0.0f, 0.5f, 0.0f}); + + // update each entity to have the same position and orientation as the + // physics simulation + for (const auto &[g, p] : boxes_) + { + g->set_position(p->position()); + g->set_orientation(p->orientation()); + } +} + +void PhysicsSample::handle_input(const iris::Event &event) +{ + if (event.is_key()) + { + const auto keyboard = event.key(); + key_map_[keyboard.key] = keyboard.state; + } + else if (event.is_mouse()) + { + static const auto sensitivity = 0.0025f; + const auto mouse = event.mouse(); + + camera_.adjust_yaw(mouse.delta_x * sensitivity); + camera_.adjust_pitch(-mouse.delta_y * sensitivity); + } +} + +std::vector PhysicsSample::render_passes() +{ + return {{&scene_, &camera_, target_}}; +} + +std::string PhysicsSample::title() const +{ + return "Physics"; +} diff --git a/samples/sample_browser/samples/physics_sample.h b/samples/sample_browser/samples/physics_sample.h new file mode 100644 index 00000000..cb67ed3e --- /dev/null +++ b/samples/sample_browser/samples/physics_sample.h @@ -0,0 +1,100 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sample.h" + +/** + * Sample showcasing the physics engine. + */ +class PhysicsSample : public Sample +{ + public: + /** + * Create a new PhysicsSample. + * + * @param window + * Window to render with. + * + * @param target + * Target to render to. + */ + PhysicsSample(iris::Window *window, iris::RenderTarget *target); + ~PhysicsSample() override = default; + + /** + * Fixed rate update function. + */ + void fixed_update() override; + + /** + * Variable rate update function. + */ + void variable_update() override; + + /** + * Handle a user input. + * + * @param event + * User input event. + */ + void handle_input(const iris::Event &event) override; + + std::vector render_passes() override; + + /** + * Title of sample. + * + * @returns + * Sample title. + */ + std::string title() const override; + + private: + /** Pointer to window. */ + iris::Window *window_; + + iris::RenderTarget *target_; + + iris::Scene scene_; + + /** Physics system */ + iris::PhysicsSystem *physics_; + + /** Transform for moving light. */ + iris::Transform light_transform_; + + /** Scene light */ + iris::PointLight *light_; + + /** Render camera. */ + iris::Camera camera_; + + /** User input key map. */ + std::map key_map_; + + /** Collection of render entity and rigid body pairs. */ + std::vector> boxes_; + + /** Character controller. */ + iris::CharacterController *character_controller_; +}; diff --git a/samples/sample_browser/samples/render_graph_sample.cpp b/samples/sample_browser/samples/render_graph_sample.cpp new file mode 100644 index 00000000..248810c0 --- /dev/null +++ b/samples/sample_browser/samples/render_graph_sample.cpp @@ -0,0 +1,217 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "render_graph_sample.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +/** + * Helper function to update camera based on user input. + * + * @param camera + * Camera to update. + * + * @param key_map + * Map of user pressed keys. + */ +void update_camera( + iris::Camera &camera, + const std::map &key_map) +{ + static auto speed = 2.0f; + iris::Vector3 velocity; + + if (key_map.at(iris::Key::W) == iris::KeyState::DOWN) + { + velocity += camera.direction() * speed; + } + + if (key_map.at(iris::Key::S) == iris::KeyState::DOWN) + { + velocity -= camera.direction() * speed; + } + + if (key_map.at(iris::Key::A) == iris::KeyState::DOWN) + { + velocity -= camera.right() * speed; + } + + if (key_map.at(iris::Key::D) == iris::KeyState::DOWN) + { + velocity += camera.right() * speed; + } + + if (key_map.at(iris::Key::Q) == iris::KeyState::DOWN) + { + velocity += camera.right().cross(camera.direction()) * speed; + } + + if (key_map.at(iris::Key::E) == iris::KeyState::DOWN) + { + velocity -= camera.right().cross(camera.direction()) * speed; + } + + camera.translate(velocity); +} + +} + +RenderGraphSample::RenderGraphSample( + iris::Window *window, + iris::RenderTarget *target) + : window_(window) + , target_(target) + , scene1_() + , scene2_() + , scene3_() + , light_transform_() + , light1_(nullptr) + , light2_(nullptr) + , sphere1_rt_(window_->create_render_target()) + , sphere2_rt_(window_->create_render_target()) + , camera_( + iris::CameraType::PERSPECTIVE, + window_->width(), + window_->height()) + , screen_camera_( + iris::CameraType::ORTHOGRAPHIC, + window_->width(), + window_->height()) + , key_map_() +{ + key_map_ = { + {iris::Key::W, iris::KeyState::UP}, + {iris::Key::A, iris::KeyState::UP}, + {iris::Key::S, iris::KeyState::UP}, + {iris::Key::D, iris::KeyState::UP}, + {iris::Key::Q, iris::KeyState::UP}, + {iris::Key::E, iris::KeyState::UP}, + }; + + auto *graph1 = scene1_.create_render_graph(); + + graph1->render_node()->set_colour_input(graph1->create( + graph1->create("brickwall.jpg"))); + graph1->render_node()->set_normal_input(graph1->create( + "brickwall_normal.jpg", iris::TextureUsage::DATA)); + + scene1_.set_ambient_light({0.15f, 0.15f, 0.15f}); + light1_ = scene1_.create_light( + iris::Vector3{0.0f, 50.0f, -50.0f}, + iris::Colour{200.0f, 200.0f, 200.0f}); + + auto &mesh_manager = iris::Root::mesh_manager(); + + auto *sphere1 = scene1_.create_entity( + graph1, + mesh_manager.load_mesh("sphere.fbx"), + iris::Transform{ + iris::Vector3{-20.0f, 0.0f, 0.0f}, + iris::Quaternion({1.0f, 0.0f, 0.0f}, 1.57079632679489661923f), + iris::Vector3{10.0f}}); + + auto *graph2 = scene2_.create_render_graph(); + + graph2->render_node()->set_colour_input( + graph2->create("brickwall.jpg")); + + scene2_.set_ambient_light({0.15f, 0.15f, 0.15f}); + light2_ = scene2_.create_light( + iris::Vector3{0.0f, 50.0f, -50.0f}, + iris::Colour{100.0f, 100.0f, 100.0f}); + + auto *sphere2 = scene2_.create_entity( + graph2, + mesh_manager.load_mesh("sphere.fbx"), + iris::Transform{ + iris::Vector3{20.0f, 0.0f, 0.0f}, + iris::Quaternion({1.0f, 0.0f, 0.0f}, 1.57079632679489661923f), + iris::Vector3{10.0f}}); + + auto *graph3 = scene3_.create_render_graph(); + + graph3->render_node()->set_colour_input(graph3->create( + graph3->create(sphere1_rt_->colour_texture()), + graph3->create( + graph3->create(sphere2_rt_->colour_texture())), + graph3->create(sphere1_rt_->depth_texture()), + graph3->create(sphere2_rt_->depth_texture()))); + + scene3_.create_entity( + graph3, + mesh_manager.sprite({}), + iris::Transform{ + iris::Vector3{}, {}, iris::Vector3{800.0f, 800.0f, 1.0f}}); + + light_transform_ = iris::Transform{light1_->position(), {}, {1.0f}}; +} + +void RenderGraphSample::fixed_update() +{ + update_camera(camera_, key_map_); + + light_transform_.set_matrix( + iris::Matrix4(iris::Quaternion{{0.0f, 1.0f, 0.0f}, -0.01f}) * + light_transform_.matrix()); + light1_->set_position(light_transform_.translation()); + light2_->set_position(light_transform_.translation()); +} + +void RenderGraphSample::variable_update() +{ +} + +void RenderGraphSample::handle_input(const iris::Event &event) +{ + if (event.is_key()) + { + const auto keyboard = event.key(); + key_map_[keyboard.key] = keyboard.state; + } + else if (event.is_mouse()) + { + static const auto sensitivity = 0.0025f; + const auto mouse = event.mouse(); + + camera_.adjust_yaw(mouse.delta_x * sensitivity); + camera_.adjust_pitch(-mouse.delta_y * sensitivity); + } +} + +std::vector RenderGraphSample::render_passes() +{ + return { + {&scene1_, &camera_, sphere1_rt_}, + {&scene2_, &camera_, sphere2_rt_}, + {&scene3_, &screen_camera_, target_}}; +} + +std::string RenderGraphSample::title() const +{ + return "Render graph"; +} diff --git a/samples/sample_browser/samples/render_graph_sample.h b/samples/sample_browser/samples/render_graph_sample.h new file mode 100644 index 00000000..d4fe866f --- /dev/null +++ b/samples/sample_browser/samples/render_graph_sample.h @@ -0,0 +1,102 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "sample.h" + +/** + * Sample showcasing the render graph. + */ +class RenderGraphSample : public Sample +{ + public: + /** + * Create a new RenderGraphSample. + * + * @param window + * Window to render with. + * + * @param target + * Target to render to. + */ + RenderGraphSample(iris::Window *window, iris::RenderTarget *target); + ~RenderGraphSample() override = default; + + /** + * Fixed rate update function. + */ + void fixed_update() override; + + /** + * Variable rate update function. + */ + void variable_update() override; + + /** + * Handle a user input. + * + * @param event + * User input event. + */ + void handle_input(const iris::Event &event) override; + + std::vector render_passes() override; + + /** + * Title of sample. + * + * @returns + * Sample title. + */ + std::string title() const override; + + private: + /** Pointer to window. */ + iris::Window *window_; + + iris::RenderTarget *target_; + + iris::Scene scene1_; + iris::Scene scene2_; + iris::Scene scene3_; + + /** Transform for moving light. */ + iris::Transform light_transform_; + + /** Scene light */ + iris::PointLight *light1_; + + /** Scene light */ + iris::PointLight *light2_; + + /** Render target. */ + iris::RenderTarget *sphere1_rt_; + + /** Render target. */ + iris::RenderTarget *sphere2_rt_; + + /** Render camera. */ + iris::Camera camera_; + + /** Composite camera. */ + iris::Camera screen_camera_; + + /** User input key map. */ + std::map key_map_; +}; diff --git a/samples/sample_browser/samples/sample.h b/samples/sample_browser/samples/sample.h new file mode 100644 index 00000000..17611440 --- /dev/null +++ b/samples/sample_browser/samples/sample.h @@ -0,0 +1,50 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include +#include + +/** + * Interface for a sample in the sample browser. + */ +class Sample +{ + public: + virtual ~Sample() = default; + + /** + * Fixed rate update function e.g. physics. + */ + virtual void fixed_update() = 0; + + /** + * Variable rate update function e.g. rendering. + */ + virtual void variable_update() = 0; + + /** + * Handle a user input. + * + * @param event + * User input event. + */ + virtual void handle_input(const iris::Event &event) = 0; + + virtual std::vector render_passes() = 0; + + /** + * Title of sample. + * + * @returns + * Sample title. + */ + virtual std::string title() const = 0; +}; diff --git a/samples/window/CMakeLists.txt b/samples/window/CMakeLists.txt new file mode 100644 index 00000000..7aec9bad --- /dev/null +++ b/samples/window/CMakeLists.txt @@ -0,0 +1,9 @@ +add_executable(window main.cpp) + +target_include_directories(window PUBLIC "${PROJECT_SOURCE_DIR}/include") +target_include_directories(window PUBLIC "${PROJECT_SOURCE_DIR}/third_party") +target_link_libraries(window iris) + +if(IRIS_PLATFORM MATCHES "WIN32") + set_target_properties(window PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDebug") +endif() diff --git a/samples/window/main.cpp b/samples/window/main.cpp new file mode 100644 index 00000000..88f51132 --- /dev/null +++ b/samples/window/main.cpp @@ -0,0 +1,181 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +/** + * Helper function to update camera based on user input. + * + * @param camera + * Camera to update. + * + * @param key_map + * Map of user pressed keys. + */ +void update_camera( + iris::Camera &camera, + const std::map &key_map) +{ + static auto speed = 2.0f; + iris::Vector3 velocity{}; + + if (key_map.at(iris::Key::W) == iris::KeyState::DOWN) + { + velocity += camera.direction() * speed; + } + + if (key_map.at(iris::Key::S) == iris::KeyState::DOWN) + { + velocity -= camera.direction() * speed; + } + + if (key_map.at(iris::Key::A) == iris::KeyState::DOWN) + { + velocity -= camera.right() * speed; + } + + if (key_map.at(iris::Key::D) == iris::KeyState::DOWN) + { + velocity += camera.right() * speed; + } + + if (key_map.at(iris::Key::Q) == iris::KeyState::DOWN) + { + velocity += camera.right().cross(camera.direction()) * speed; + } + + if (key_map.at(iris::Key::E) == iris::KeyState::DOWN) + { + velocity -= camera.right().cross(camera.direction()) * speed; + } + + camera.translate(velocity); +} + +} + +void go(int, char **) +{ + iris::ResourceLoader::instance().set_root_directory("assets"); + + std::map key_map = { + {iris::Key::W, iris::KeyState::UP}, + {iris::Key::A, iris::KeyState::UP}, + {iris::Key::S, iris::KeyState::UP}, + {iris::Key::D, iris::KeyState::UP}, + {iris::Key::Q, iris::KeyState::UP}, + {iris::Key::E, iris::KeyState::UP}, + }; + + auto window = iris::Root::window_manager().create_window(800u, 800u); + iris::Camera camera{iris::CameraType::PERSPECTIVE, 800u, 800u}; + iris::Camera orth{iris::CameraType::ORTHOGRAPHIC, 800u, 800u}; + auto *rt = window->create_render_target(800u, 800u); + camera.set_position({0.0f, 0.0f, 800.0f}); + + auto &mesh_manager = iris::Root::mesh_manager(); + + auto scene = std::make_unique(); + auto *light = scene->create_light( + iris::Vector3{-1.0f, -1.0f, 0.0f}); + auto *e = scene->create_entity( + nullptr, + mesh_manager.cube(iris::Colour(1.0f, 0.0f, 1.0f)), + iris::Transform{{0.5f, 0.0f, 0.0f}, {}, {100.0f}}); + + auto *rg = scene->create_render_graph(); + rg->render_node()->set_colour_input( + rg->create("crate.png")); + + scene->create_entity( + rg, + mesh_manager.cube(iris::Colour(1.0f, 0.0f, 0.0f)), + iris::Transform{{100.0f, 100.0f, 0.0f}, {}, {100.0f}}); + scene->set_ambient_light({0.2f, 0.2f, 0.2f, 1.0f}); + + iris::RenderPass pass1{scene.get(), &camera, nullptr}; + + iris::Transform light_transform{light->direction(), {}, {1.0f}}; + + window->set_render_passes({pass1}); + + auto rot = 0.0f; + auto running = true; + + do + { + auto event = window->pump_event(); + while (event) + { + std::cout << "handled" << std::endl; + if (event->is_quit() || event->is_key(iris::Key::ESCAPE)) + { + running = false; + break; + } + else if (event->is_key()) + { + const auto keyboard = event->key(); + key_map[keyboard.key] = keyboard.state; + } + else if (event->is_mouse()) + { + static const auto sensitivity = 0.0025f; + const auto mouse = event->mouse(); + + camera.adjust_yaw(mouse.delta_x * sensitivity); + camera.adjust_pitch(-mouse.delta_y * sensitivity); + } + + event = window->pump_event(); + } + + update_camera(camera, key_map); + + rot += 0.01f; + e->set_orientation({{0.0f, 1.0f, 0.0f}, rot}); + + light_transform.set_matrix( + iris::Matrix4(iris::Quaternion{{0.0f, 1.0f, 0.0f}, -0.01f}) * + light_transform.matrix()); + light->set_direction(light_transform.translation()); + + window->render(); + // window->render(pipeline2); + } while (running); +} + +int main(int argc, char **argv) +{ + iris::start_debug(argc, argv, go); + + return 0; +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 00000000..26b73834 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,147 @@ +add_library(iris STATIC "") + +add_subdirectory("core") +add_subdirectory("events") +add_subdirectory("graphics") +add_subdirectory("jobs") +add_subdirectory("log") +add_subdirectory("networking") +add_subdirectory("physics") + +add_library(iris::iris ALIAS iris) +generate_export_header(iris) + +# hide symbols +set_target_properties(iris PROPERTIES + CMAKE_CXX_VISIBILITY_PRESET hidden + CMAKE_VISIBILITY_INLINES_HIDDEN 1) + +target_include_directories( + iris + PUBLIC $ + $) + +target_include_directories( + iris SYSTEM + PRIVATE ${stb_SOURCE_DIR} ${bullet_SOURCE_DIR}/src) + +# configure ersion file +configure_file(${PROJECT_SOURCE_DIR}/include/iris/iris_version.h.in ${PROJECT_SOURCE_DIR}/include/iris/iris_version.h) + +# set macos/ios framework includes +if(IRIS_PLATFORM MATCHES "MACOS") + target_link_libraries(iris PUBLIC "-framework AppKit -framework CoreFoundation -framework CoreGraphics -framework Metal -framework MetalKit -framework MetalPerformanceShaders -framework QuartzCore") +elseif(IRIS_PLATFORM MATCHES "IOS") + target_link_libraries(iris PUBLIC "-framework AppKit -framework CoreFoundation -framework UIKit -framework Foundation -framework Metal -framework MetalKit -framework MetalPerformanceShaders -framework QuartzCore") +endif() + +# handle platform specific setup including setting default graphics apis +if(IRIS_PLATFORM MATCHES "MACOS") + target_compile_definitions(iris PUBLIC IRIS_PLATFORM_MACOS) + target_compile_definitions(iris PUBLIC IRIS_ARCH_X86_64) + if(NOT IRIS_JOBS_API) + set(IRIS_JOBS_API "FIBERS") + endif() +elseif(IRIS_PLATFORM MATCHES "IOS") + target_compile_definitions(iris PUBLIC IRIS_PLATFORM_IOS) + target_compile_definitions(iris PUBLIC IRIS_ARCH_ARM64) + set(IRIS_ARCH "ARM64") + if(NOT IRIS_JOBS_API) + set(IRIS_JOBS_API "THREADS") + endif() +elseif(IRIS_PLATFORM MATCHES "WIN32") + target_compile_definitions(iris PUBLIC IRIS_PLATFORM_WIN32) + target_compile_definitions(iris PUBLIC IRIS_ARCH_X86_64) + target_compile_definitions(iris PUBLIC NOMINMAX) + if(NOT IRIS_JOBS_API) + set(IRIS_JOBS_API "THREADS") + endif() +else() + message(FATAL_ERROR "Unsupported platform") +endif() + + +if(IRIS_PLATFORM MATCHES "MACOS" OR IRIS_PLATFORM MATCHES "WINDOWS") + find_package(OpenGL REQUIRED) + target_include_directories(iris SYSTEM PRIVATE ${OPENGL_INCLUDE_DIR}) + if(IRIS_PLATFORM MATCHES "MACOS") + target_link_libraries(iris PUBLIC "-framework OpenGL") + # opengl is technically deprecated on macos, so silence the deprecation + # warnings + target_compile_definitions(iris PRIVATE GL_SILENCE_DEPRECATION) + endif() +endif() + +message(STATUS "Building iris-${CMAKE_PROJECT_VERSION} for ${IRIS_PLATFORM} (${IRIS_JOBS_API})") + +target_link_libraries(iris PUBLIC LinearMath BulletDynamics BulletCollision assimp) + +if(IRIS_PLATFORM MATCHES "WIN32") + # on windows SYSTEM includes do not prevent warnings - so we add bullet using + # the /external flag + target_compile_options(iris PRIVATE /W4 /WX /experimental:external + /external:W0 /external:I ${bullet_SOURCE_DIR}) + target_link_options(iris PUBLIC /subsystem:windows /ENTRY:mainCRTStartup) + target_link_libraries(iris PRIVATE DirectX-Headers) + + set_target_properties(iris PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDebug") + set_target_properties(assimp PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDebug") + set_target_properties(LinearMath PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDebug") + set_target_properties(BulletDynamics PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDebug") + set_target_properties(BulletCollision PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDebug") + + install( + TARGETS iris + LinearMath + BulletDynamics + BulletCollision + assimp + zlibstatic + IrrXML + DirectX-Headers + EXPORT iris-targets + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +else() + target_compile_options(iris PRIVATE -Wall -Werror -pedantic -glldb -fobjc-arc) + + install( + TARGETS iris assimp zlibstatic IrrXML BulletDynamics BulletCollision LinearMath + EXPORT iris-targets + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +endif() + +# various install commands to ensure all files get put in the right place + +install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/iris + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + +configure_package_config_file( + ${PROJECT_SOURCE_DIR}/cmake/iris-config.cmake.in + ${PROJECT_BINARY_DIR}/cmake/iris-config.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/iris) + +write_basic_package_version_file( + iris-version.cmake + VERSION ${PACKAGE_VERSION} + COMPATIBILITY AnyNewerVersion) + +install( + EXPORT iris-targets + FILE iris-targets.cmake + NAMESPACE iris:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/iris) + +install(FILES ${PROJECT_BINARY_DIR}/cmake/iris-config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/iris-version.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/iris) + + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/iris_export.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/iris) + +export(EXPORT iris-targets + FILE ${PROJECT_BINARY_DIR}/cmake/iris-targets.cmake + NAMESPACE iris::) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt new file mode 100644 index 00000000..7af75e45 --- /dev/null +++ b/src/core/CMakeLists.txt @@ -0,0 +1,37 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/core") + +if(IRIS_PLATFORM MATCHES "MACOS") + add_subdirectory("macos") +elseif(IRIS_PLATFORM MATCHES "IOS") + add_subdirectory("ios") +elseif(IRIS_PLATFORM MATCHES "WIN32") + add_subdirectory("win32") +endif() + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/auto_release.h + ${INCLUDE_ROOT}/camera.h + ${INCLUDE_ROOT}/camera_type.h + ${INCLUDE_ROOT}/colour.h + ${INCLUDE_ROOT}/data_buffer.h + ${INCLUDE_ROOT}/error_handling.h + ${INCLUDE_ROOT}/exception.h + ${INCLUDE_ROOT}/looper.h + ${INCLUDE_ROOT}/matrix4.h + ${INCLUDE_ROOT}/quaternion.h + ${INCLUDE_ROOT}/random.h + ${INCLUDE_ROOT}/resource_loader.h + ${INCLUDE_ROOT}/root.h + ${INCLUDE_ROOT}/start.h + ${INCLUDE_ROOT}/static_buffer.h + ${INCLUDE_ROOT}/thread.h + ${INCLUDE_ROOT}/transform.h + ${INCLUDE_ROOT}/utils.h + ${INCLUDE_ROOT}/vector3.h + camera.cpp + exception.cpp + looper.cpp + random.cpp + root.cpp + transform.cpp + utils.cpp) diff --git a/src/core/camera.cpp b/src/core/camera.cpp new file mode 100644 index 00000000..565ff4f7 --- /dev/null +++ b/src/core/camera.cpp @@ -0,0 +1,168 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/camera.h" + +#include + +#include "core/camera_type.h" +#include "core/matrix4.h" +#include "core/quaternion.h" +#include "core/vector3.h" +#include "log/log.h" + +namespace +{ + +/** + * Helper method to create a direction vector from a pitch and yaw + * + * @param pitch + * pitch (in radians) of camera + * + * @param yaw + * yaw (in radians) of camera + * + * @returns + * A new direction vector for the camera + */ +iris::Vector3 create_direction(float pitch, float yaw) +{ + iris::Vector3 direction; + + direction.x = std::cos(yaw) * std::cos(pitch); + direction.y = std::sin(pitch); + direction.z = std::sin(yaw) * std::cos(pitch); + + direction.normalise(); + + return direction; +} + +} + +namespace iris +{ + +Camera::Camera(CameraType type, std::uint32_t width, std::uint32_t height, std::uint32_t depth) + : position_(0.0f, 0.0f, 100.0f) + , direction_(0.0f, 0.0f, -1.0f) + , up_(0.0f, 1.0f, 0.0f) + , view_() + , projection_() + , pitch_(0.0f) + , yaw_(-3.141592654f / 2.0f) + , type_(type) +{ + const auto width_f = static_cast(width); + const auto height_f = static_cast(height); + const auto depth_f = static_cast(depth); + + switch (type_) + { + case CameraType::PERSPECTIVE: + projection_ = Matrix4::make_perspective_projection(0.785398f, width_f, height_f, 0.1f, depth_f); + break; + case CameraType::ORTHOGRAPHIC: + projection_ = Matrix4::make_orthographic_projection(width_f, height_f, depth_f); + break; + } + + direction_ = create_direction(pitch_, yaw_); + view_ = Matrix4::make_look_at(position_, position_ + direction_, up_); + + LOG_ENGINE_INFO("camera", "constructed"); +} + +void Camera::translate(const Vector3 &translate) +{ + position_ += translate; + view_ = Matrix4::make_look_at(position_, position_ + direction_, up_); +} + +void Camera::set_view(const Matrix4 &view) +{ + view_ = view; +} + +Vector3 Camera::position() const +{ + return position_; +} + +Quaternion Camera::orientation() const +{ + return {yaw_, pitch_, 0.0f}; +} + +Vector3 Camera::direction() const +{ + return direction_; +} + +Vector3 Camera::right() const +{ + return Vector3::normalise(Vector3::cross(direction_, up_)); +} + +Matrix4 Camera::view() const +{ + return view_; +} + +Matrix4 Camera::projection() const +{ + return projection_; +} + +float Camera::yaw() const +{ + return yaw_; +} + +void Camera::set_yaw(float yaw) +{ + yaw_ = yaw; + + direction_ = create_direction(pitch_, yaw_); + view_ = Matrix4::make_look_at(position_, position_ + direction_, up_); +} + +void Camera::adjust_yaw(float adjust) +{ + set_yaw(yaw_ + adjust); +} + +float Camera::pitch() const +{ + return pitch_; +} + +void Camera::set_pitch(float pitch) +{ + pitch_ = pitch; + + direction_ = create_direction(pitch_, yaw_); + view_ = Matrix4::make_look_at(position_, position_ + direction_, up_); +} + +void Camera::set_position(const Vector3 &position) +{ + position_ = position; + view_ = Matrix4::make_look_at(position_, position_ + direction_, up_); +} + +void Camera::adjust_pitch(float adjust) +{ + set_pitch(pitch_ + adjust); +} + +CameraType Camera::type() const +{ + return type_; +} + +} diff --git a/src/core/default/resource_loader.cpp b/src/core/default/resource_loader.cpp new file mode 100644 index 00000000..9ada2df7 --- /dev/null +++ b/src/core/default/resource_loader.cpp @@ -0,0 +1,59 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/resource_loader.h" + +#include +#include +#include + +#include "core/error_handling.h" + +namespace iris +{ + +ResourceLoader::ResourceLoader() + : root_(".") +{ +} + +ResourceLoader &ResourceLoader::instance() +{ + static ResourceLoader loader{}; + return loader; +} + +const DataBuffer &ResourceLoader::load(const std::string &resource) +{ + // lookup resource + auto loaded_resource = resources_.find(resource); + + // if not found load from disk, treat resource as a path relative to + // root + if (loaded_resource == std::cend(resources_)) + { + std::stringstream strm{}; + std::fstream f(root_ / std::filesystem::path(resource), std::ios::in | std::ios::binary); + + strm << f.rdbuf(); + + ensure(f.good() && !f.bad(), "failed to read file"); + + const auto str = strm.str(); + const auto *str_ptr = reinterpret_cast(str.data()); + + const auto [iter, _] = resources_.insert({resource, {str_ptr, str_ptr + str.length()}}); + loaded_resource = iter; + } + + return loaded_resource->second; +} + +void ResourceLoader::set_root_directory(const std::filesystem::path &root) +{ + root_ = root; +} +} diff --git a/src/core/exception.cpp b/src/core/exception.cpp new file mode 100644 index 00000000..f2ed7c83 --- /dev/null +++ b/src/core/exception.cpp @@ -0,0 +1,20 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/exception.h" + +#include +#include + +namespace iris +{ + +Exception::Exception(const std::string &what) + : std::runtime_error(what) +{ +} + +} diff --git a/src/core/ios/CMakeLists.txt b/src/core/ios/CMakeLists.txt new file mode 100644 index 00000000..69989095 --- /dev/null +++ b/src/core/ios/CMakeLists.txt @@ -0,0 +1,7 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/core/ios") + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/utility.h + ../macos/macos_ios_utility.mm + resource_loader.mm + start.mm) diff --git a/src/core/ios/resource_loader.mm b/src/core/ios/resource_loader.mm new file mode 100644 index 00000000..0b28f6f6 --- /dev/null +++ b/src/core/ios/resource_loader.mm @@ -0,0 +1,68 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/resource_loader.h" + +#include +#include + +#import + +#include "core/exception.h" +#include "core/macos/macos_ios_utility.h" + +namespace iris +{ + +ResourceLoader::ResourceLoader(){}; + +ResourceLoader &ResourceLoader::instance() +{ + static ResourceLoader loader{}; + return loader; +} + +const DataBuffer &ResourceLoader::load(const std::string &resource) +{ + // lookup resource + auto loaded_resource = resources_.find(resource); + + // if not found load from disk, treat resource as a path relative to + // the app bundle resource path + if (loaded_resource == std::cend(resources_)) + { + const auto *bundle = [NSBundle mainBundle]; + if (bundle == nullptr) + { + throw Exception("could not get main bundle"); + } + + const auto *dir = [bundle resourcePath]; + if (dir == nullptr) + { + throw Exception("could not resolve path to resouce path"); + } + + const auto *resource_ns = core::utility::string_to_nsstring(resource); + auto *parts = [NSArray arrayWithObjects:dir, resource_ns, (void *)nil]; + const auto *path = [NSString pathWithComponents:parts]; + const auto *cpath = [path fileSystemRepresentation]; + + std::stringstream strm{}; + std::fstream f(cpath, std::ios::in | std::ios::binary); + strm << f.rdbuf(); + + const auto str = strm.str(); + const auto *ptr = reinterpret_cast(str.data()); + + const auto [iter, _] = resources_.insert({resource, DataBuffer(ptr, ptr + str.size())}); + loaded_resource = iter; + } + + return loaded_resource->second; +} + +} diff --git a/src/core/ios/start.mm b/src/core/ios/start.mm new file mode 100644 index 00000000..317e106f --- /dev/null +++ b/src/core/ios/start.mm @@ -0,0 +1,98 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/start.h" + +#include + +#import + +#import "graphics/ios/app_delegate.h" + +#include "core/root.h" +#include "graphics/ios/ios_window_manager.h" +#include "graphics/metal/metal_mesh_manager.h" +#include "graphics/metal/metal_texture_manager.h" +#include "iris_version.h" +#include "jobs/thread/thread_job_system_manager.h" +#include "log/emoji_formatter.h" +#include "log/log.h" +#include "log/logger.h" +#include "physics/bullet/bullet_physics_manager.h" + +namespace +{ + +/** + * Register apis and set default.s + */ +void register_apis() +{ + iris::Root::register_graphics_api( + "metal", + std::make_unique(), + std::make_unique(), + std::make_unique()); + iris::Root::set_graphics_api("metal"); + + iris::Root::register_physics_api("bullet", std::make_unique()); + iris::Root::set_physics_api("bullet"); + + iris::Root::register_jobs_api("thread", std::make_unique()); + iris::Root::set_jobs_api("thread"); +} + +} + +namespace iris +{ + +// globals so we can call back into game +// nasty but effective +std::function g_entry; +int g_argc; +char **g_argv; + +void start(int argc, char **argv, std::function entry) +{ + // xcode doesn't support ANSI colour codes so we default to the emoji + // formatter + Logger::instance().set_Formatter(); + + register_apis(); + + LOG_ENGINE_INFO("start", "engine start"); + + // save off supplied variables for use later + g_entry = entry; + g_argc = argc; + g_argv = argv; + + @autoreleasepool + { + // start the main ios application + // this is why we have to store the function arguments as globals as + // we have no way of accessing them in the AppDelegate + ::UIApplicationMain(argc, argv, nil, ::NSStringFromClass([AppDelegate class])); + + Root::reset(); + } +} + +void start_debug(int argc, char **argv, std::function entry) +{ + // enable engine logging + Logger::instance().set_Formatter(); + Logger::instance().set_log_engine(true); + + LOG_ENGINE_INFO("start", "engine start (with debugging)"); + + register_apis(); + + start(argc, argv, entry); +} + +} diff --git a/src/core/looper.cpp b/src/core/looper.cpp new file mode 100644 index 00000000..1d8e83f8 --- /dev/null +++ b/src/core/looper.cpp @@ -0,0 +1,55 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/looper.h" + +#include + +namespace iris +{ + +Looper::Looper( + std::chrono::microseconds clock, + std::chrono::microseconds timestep, + LoopFunction fixed_timestep, + LoopFunction variable_timestep) + : clock_(clock) + , timestep_(timestep) + , fixed_timestep_(fixed_timestep) + , variable_timestep_(variable_timestep) +{ +} + +void Looper::run() +{ + auto run = true; + auto start = std::chrono::steady_clock::now(); + std::chrono::steady_clock::duration accumulator(0); + + do + { + // calculate duration of last frame + const auto end = std::chrono::steady_clock::now(); + const auto frame_time = end - start; + start = end; + + // variable time step function produces time + accumulator += frame_time; + + // fixed time step function consumed time + while (run && (accumulator >= timestep_)) + { + run &= fixed_timestep_(clock_, timestep_); + + accumulator -= timestep_; + clock_ += timestep_; + } + + run &= variable_timestep_(clock_, std::chrono::duration_cast(frame_time)); + } while (run); +} + +} diff --git a/src/core/macos/CMakeLists.txt b/src/core/macos/CMakeLists.txt new file mode 100644 index 00000000..8399eb6d --- /dev/null +++ b/src/core/macos/CMakeLists.txt @@ -0,0 +1,12 @@ +set(DEFAULT_ROOT "${PROJECT_SOURCE_DIR}/src/core/default") +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/core/macos") + +target_sources(iris PRIVATE + ${DEFAULT_ROOT}/resource_loader.cpp + ${INCLUDE_ROOT}/macos_ios_utility.h + ${INCLUDE_ROOT}/utility.h + macos_ios_utility.mm + semaphore.cpp + start.mm + static_buffer.cpp + thread.cpp) diff --git a/src/core/macos/macos_ios_utility.mm b/src/core/macos/macos_ios_utility.mm new file mode 100644 index 00000000..38b623f0 --- /dev/null +++ b/src/core/macos/macos_ios_utility.mm @@ -0,0 +1,30 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/macos/macos_ios_utility.h" + +#import +#import + +namespace iris::core::utility +{ + +// common utilities + +NSString *string_to_nsstring(const std::string &str) +{ + return [NSString stringWithUTF8String:str.c_str()]; +} + +} + +// platform specific implementations + +#if defined(IRIS_PLATFORM_MACOS) +#include "core/macos/utility.h" +#elif defined(IRIS_PLATFORM_IOS) +#include "core/ios/utility.h" +#endif diff --git a/src/core/macos/semaphore.cpp b/src/core/macos/semaphore.cpp new file mode 100644 index 00000000..3eb11f87 --- /dev/null +++ b/src/core/macos/semaphore.cpp @@ -0,0 +1,48 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/semaphore.h" + +#include +#include + +#include + +#include "core/auto_release.h" + +namespace iris +{ + +struct Semaphore::implementation +{ + AutoRelease<::dispatch_semaphore_t, nullptr> semaphore; + std::atomic count; +}; + +Semaphore::Semaphore(std::ptrdiff_t initial) + : impl_(std::make_unique()) +{ + impl_->semaphore = {::dispatch_semaphore_create(initial), ::dispatch_release}; + impl_->count = initial; +} + +Semaphore::~Semaphore() = default; +Semaphore::Semaphore(Semaphore &&) = default; +Semaphore &Semaphore::operator=(Semaphore &&) = default; + +void Semaphore::release() +{ + ++impl_->count; + ::dispatch_semaphore_signal(impl_->semaphore); +} + +void Semaphore::acquire() +{ + ::dispatch_semaphore_wait(impl_->semaphore, DISPATCH_TIME_FOREVER); + --impl_->count; +} + +} diff --git a/src/core/macos/start.mm b/src/core/macos/start.mm new file mode 100644 index 00000000..1849615c --- /dev/null +++ b/src/core/macos/start.mm @@ -0,0 +1,84 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/start.h" + +#include + +#include "core/root.h" +#include "graphics/macos/macos_window_manager.h" +#include "graphics/metal/metal_mesh_manager.h" +#include "graphics/metal/metal_texture_manager.h" +#include "graphics/opengl/opengl_mesh_manager.h" +#include "graphics/opengl/opengl_texture_manager.h" +#include "iris_version.h" +#include "jobs/fiber/fiber_job_system_manager.h" +#include "jobs/thread/thread_job_system_manager.h" +#include "log/log.h" +#include "log/logger.h" +#include "physics/bullet/bullet_physics_manager.h" + +namespace +{ + +void register_apis() +{ + iris::Root::register_graphics_api( + "metal", + std::make_unique(), + std::make_unique(), + std::make_unique()); + + iris::Root::register_graphics_api( + "opengl", + std::make_unique(), + std::make_unique(), + std::make_unique()); + + iris::Root::set_graphics_api("metal"); + + iris::Root::register_physics_api("bullet", std::make_unique()); + + iris::Root::set_physics_api("bullet"); + + iris::Root::register_jobs_api("thread", std::make_unique()); + + iris::Root::register_jobs_api("fiber", std::make_unique()); + + iris::Root::set_jobs_api("fiber"); +} + +} + +namespace iris +{ + +void start(int argc, char **argv, std::function entry) +{ + LOG_ERROR("start", "engine start {}", IRIS_VERSION_STR); + + register_apis(); + + entry(argc, argv); + + Root::reset(); +} + +void start_debug(int argc, char **argv, std::function entry) +{ + // enable engine logging + Logger::instance().set_log_engine(true); + + LOG_ENGINE_INFO("start", "engine start (with debugging) {}", IRIS_VERSION_STR); + + register_apis(); + + entry(argc, argv); + + Root::reset(); +} + +} diff --git a/src/core/macos/static_buffer.cpp b/src/core/macos/static_buffer.cpp new file mode 100644 index 00000000..2181667f --- /dev/null +++ b/src/core/macos/static_buffer.cpp @@ -0,0 +1,75 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/static_buffer.h" + +#include +#include +#include +#include + +#include +#include + +#include "core/error_handling.h" + +namespace iris +{ + +struct StaticBuffer::implementation +{ + std::byte *allocated_region; + std::size_t allocated_size; + std::byte *usable_region; + std::size_t usable_size; +}; + +StaticBuffer::StaticBuffer(std::size_t pages) + : impl_(std::make_unique()) +{ + // calculate amount of bytes to allocate, including guard pages + impl_->allocated_size = (pages + 2u) * page_size(); + + impl_->allocated_region = static_cast( + ::mmap(0, impl_->allocated_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0)); + + expect(impl_->allocated_region != MAP_FAILED, "failed to mmap memory"); + + // set head guard page + const auto remove_protection = ::mprotect(impl_->allocated_region, page_size(), PROT_NONE); + expect(remove_protection != -1, "failed to set head guard page"); + + // set tail guard page + const auto set_protection = + ::mprotect(impl_->allocated_region + ((pages + 1u) * page_size()), page_size(), PROT_NONE); + expect(set_protection != -1, "failed to set tail guard page"); + + // calculate usable region pointer and size + impl_->usable_region = impl_->allocated_region + page_size(); + impl_->usable_size = impl_->allocated_size - (2u * page_size()); +} + +StaticBuffer::~StaticBuffer() +{ + ::munmap(impl_->allocated_region, impl_->allocated_size); +} + +std::size_t StaticBuffer::page_size() +{ + return static_cast(::getpagesize()); +} + +StaticBuffer::operator std::byte *() const +{ + return impl_->usable_region; +} + +std::size_t StaticBuffer::size() const +{ + return impl_->usable_size; +} + +} diff --git a/src/core/macos/thread.cpp b/src/core/macos/thread.cpp new file mode 100644 index 00000000..d3484517 --- /dev/null +++ b/src/core/macos/thread.cpp @@ -0,0 +1,54 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/thread.h" + +#include + +#include +#include +#include + +#include "core/error_handling.h" + +namespace iris +{ + +Thread::Thread() + : thread_() +{ +} + +bool Thread::joinable() const +{ + return thread_.joinable(); +} + +void Thread::join() +{ + thread_.join(); +} + +std::thread::id Thread::get_id() const +{ + return thread_.get_id(); +} + +void Thread::bind_to_core(std::size_t core) +{ + ensure(core < std::thread::hardware_concurrency(), "invalid core id"); + + // convert pthread to mach_thread + thread_affinity_policy_data_t policy = {static_cast(core)}; + const auto mach_thread = pthread_mach_thread_np(thread_.native_handle()); + + // set affinity policy, this is merely a suggestion to the kernel + const auto set_policy = + ::thread_policy_set(mach_thread, THREAD_AFFINITY_POLICY, reinterpret_cast(&policy), 1); + expect(set_policy == KERN_SUCCESS, "failed to bind thread to core"); +} + +} diff --git a/src/core/random.cpp b/src/core/random.cpp new file mode 100644 index 00000000..b9f2cd09 --- /dev/null +++ b/src/core/random.cpp @@ -0,0 +1,58 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/random.h" + +#include + +namespace +{ + +// ensure each thread gets its own random_device +thread_local std::random_device device; + +/** + * Helper function to generate a random value with a given distribution. + * + * @param distribution + * Distribution of random value. + * + * @returns + * Random value with given distribution. + */ +template +typename T::result_type generate(T distribution) +{ + std::mt19937 engine(device()); + return distribution(engine); +} + +} + +namespace iris +{ + +std::uint32_t random_uint32(std::uint32_t min, std::uint32_t max) +{ + return generate(std::uniform_int_distribution<>(min, max)); +} + +std::int32_t random_int32(std::int32_t min, std::int32_t max) +{ + return generate(std::uniform_int_distribution<>(min, max)); +} + +float random_float(float min, float max) +{ + return generate(std::uniform_real_distribution(min, max)); +} + +bool flip_coin(float bias) +{ + return generate(std::bernoulli_distribution(bias)); +} + +} diff --git a/src/core/root.cpp b/src/core/root.cpp new file mode 100644 index 00000000..9adb4f15 --- /dev/null +++ b/src/core/root.cpp @@ -0,0 +1,265 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/root.h" + +#include +#include +#include +#include + +#include "core/error_handling.h" +#include "graphics/mesh_manager.h" +#include "graphics/texture_manager.h" +#include "graphics/window_manager.h" +#include "jobs/job_system_manager.h" +#include "physics/physics_manager.h" + +namespace iris +{ + +Root::Root() + : graphics_api_managers_() + , graphics_api_() + , physics_api_managers_() + , physics_api_() + , jobs_api_managers_() + , jobs_api_() +{ +} + +Root &Root::instance() +{ + static Root root{}; + return root; +} + +WindowManager &Root::window_manager() +{ + return instance().window_manager_impl(); +} + +MeshManager &Root::mesh_manager() +{ + return instance().mesh_manager_impl(); +} + +TextureManager &Root::texture_manager() +{ + return instance().texture_manager_impl(); +} + +PhysicsManager &Root::physics_manager() +{ + return instance().physics_manager_impl(); +} + +JobSystemManager &Root::jobs_manager() +{ + return instance().jobs_manager_impl(); +} + +void Root::register_graphics_api( + const std::string &api, + std::unique_ptr window_manager, + std::unique_ptr mesh_manager, + std::unique_ptr texture_manager) +{ + return instance().register_graphics_api_impl( + api, std::move(window_manager), std::move(mesh_manager), std::move(texture_manager)); +} + +std::string Root::graphics_api() +{ + return instance().graphics_api_impl(); +} + +void Root::set_graphics_api(const std::string &api) +{ + return instance().set_graphics_api_impl(api); +} + +std::vector Root::registered_graphics_apis() +{ + return instance().registered_graphics_apis_impl(); +} + +void Root::register_physics_api(const std::string &api, std::unique_ptr physics_manager) +{ + return instance().register_physics_api_impl(api, std::move(physics_manager)); +} + +std::string Root::physics_api() +{ + return instance().physics_api_impl(); +} + +void Root::set_physics_api(const std::string &api) +{ + return instance().set_physics_api_impl(api); +} + +std::vector Root::registered_physics_apis() +{ + return instance().registered_physics_apis_impl(); +} + +void Root::register_jobs_api(const std::string &api, std::unique_ptr jobs_manager) +{ + return instance().register_jobs_api_impl(api, std::move(jobs_manager)); +} + +std::string Root::jobs_api() +{ + return instance().jobs_api_impl(); +} + +void Root::set_jobs_api(const std::string &api) +{ + return instance().set_jobs_api_impl(api); +} + +std::vector Root::registered_jobs_apis() +{ + return instance().registered_jobs_apis_impl(); +} + +void Root::reset() +{ + instance().reset_impl(); +} + +WindowManager &Root::window_manager_impl() const +{ + return *graphics_api_managers_.at(graphics_api_).window_manager; +} + +MeshManager &Root::mesh_manager_impl() const +{ + return *graphics_api_managers_.at(graphics_api_).mesh_manager; +} + +TextureManager &Root::texture_manager_impl() const +{ + return *graphics_api_managers_.at(graphics_api_).texture_manager; +} + +PhysicsManager &Root::physics_manager_impl() const +{ + return *physics_api_managers_.at(physics_api_); +} + +JobSystemManager &Root::jobs_manager_impl() const +{ + return *jobs_api_managers_.at(jobs_api_); +} + +void Root::register_graphics_api_impl( + const std::string &api, + std::unique_ptr window_manager, + std::unique_ptr mesh_manager, + std::unique_ptr texture_manager) +{ + GraphicsApiManagers managers{}; + managers.window_manager = std::move(window_manager); + managers.mesh_manager = std::move(mesh_manager); + managers.texture_manager = std::move(texture_manager); + + graphics_api_managers_[api] = std::move(managers); +} + +std::string Root::graphics_api_impl() const +{ + return graphics_api_; +} + +void Root::set_graphics_api_impl(const std::string &api) +{ + ensure(graphics_api_managers_.count(api) != 0u, "api not registered"); + + graphics_api_ = api; +} + +std::vector Root::registered_graphics_apis_impl() const +{ + std::vector apis{}; + + for (const auto &[api, _] : graphics_api_managers_) + { + apis.emplace_back(api); + } + + return apis; +} + +void Root::register_physics_api_impl(const std::string &api, std::unique_ptr physics_manager) +{ + physics_api_managers_[api] = std::move(physics_manager); +} + +std::string Root::physics_api_impl() const +{ + return physics_api_; +} + +void Root::set_physics_api_impl(const std::string &api) +{ + ensure(physics_api_managers_.count(api) != 0u, "api not registered"); + + physics_api_ = api; +} + +std::vector Root::registered_physics_apis_impl() const +{ + std::vector apis{}; + + for (const auto &[api, _] : physics_api_managers_) + { + apis.emplace_back(api); + } + + return apis; +} + +void Root::register_jobs_api_impl(const std::string &api, std::unique_ptr jobs_manager) +{ + jobs_api_managers_[api] = std::move(jobs_manager); +} + +std::string Root::jobs_api_impl() const +{ + return jobs_api_; +} + +void Root::set_jobs_api_impl(const std::string &api) +{ + ensure(jobs_api_managers_.count(api) != 0u, "api not registered"); + + jobs_api_ = api; + + jobs_manager_impl().create_job_system(); +} + +std::vector Root::registered_jobs_apis_impl() const +{ + std::vector apis{}; + + for (const auto &[api, _] : jobs_api_managers_) + { + apis.emplace_back(api); + } + + return apis; +} + +void Root::reset_impl() +{ + physics_api_managers_.clear(); + graphics_api_managers_.clear(); + jobs_api_managers_.clear(); +} + +} diff --git a/src/core/transform.cpp b/src/core/transform.cpp new file mode 100644 index 00000000..632247d0 --- /dev/null +++ b/src/core/transform.cpp @@ -0,0 +1,215 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/transform.h" + +#include + +#include "core/matrix4.h" +#include "core/quaternion.h" +#include "core/vector3.h" + +/** + * Convert a transformation matrix into translation, rotation and scale + * components. + * + * @param matrix + * Matrix to decompose. + * + * @returns + * Tuple of + */ +std::tuple decompose(iris::Matrix4 matrix) +{ + // extract translation + const iris::Vector3 translation = matrix.column(3u); + + // extract scale + const iris::Vector3 scale = { + matrix.column(0u).magnitude(), matrix.column(1u).magnitude(), matrix.column(2u).magnitude()}; + + // convert upper left 3x3 matrix to rotation matrix + matrix[0] /= scale.x; + matrix[1] /= scale.y; + matrix[2] /= scale.z; + matrix[4] /= scale.x; + matrix[5] /= scale.y; + matrix[6] /= scale.z; + matrix[8] /= scale.x; + matrix[9] /= scale.y; + matrix[10] /= scale.z; + + matrix[3] = 0.0f; + matrix[7] = 0.0f; + matrix[11] = 0.0f; + + iris::Quaternion rotation{}; + + // the following code is cribbed from OgreQuaternion.cpp FromRotatinMatrix + // commit: e1c3732c51f9099bed10d36805b738015adc8f47 + // which in turn is based on: + // Algorithm in Ken Shoemake's article in 1987 SIGGRAPH course notes + // article "Quaternion Calculus and Fast Animation". + + const auto trace = matrix[0] + matrix[5] + matrix[10]; + float root = 0.0f; + + if (trace > 0.0f) + { + root = std::sqrt(trace + 1.0f); + rotation.w = 0.5f * root; + root = 0.5f / root; + rotation.x = (matrix[9] - matrix[6]) * root; + rotation.y = (matrix[2] - matrix[8]) * root; + rotation.z = (matrix[4] - matrix[1]) * root; + } + else + { + static std::size_t next[3] = {1, 2, 0}; + std::size_t i = 0; + + if (matrix[5] > matrix[0]) + { + i = 1; + } + + if (matrix[10] > matrix[(i * 4u) + i]) + { + i = 2; + } + + const auto j = next[i]; + const auto k = next[j]; + + root = std::sqrt(matrix[(i * 4u) + i] - matrix[(j * 4u) + j] - matrix[(k * 4u) + k] + 1.0f); + + float *quat[3] = {&rotation.x, &rotation.y, &rotation.z}; + + *quat[i] = 0.5f * root; + root = 0.5f / root; + rotation.w = (matrix[(k * 4u) + j] - matrix[(j * 4u) + k]) * root; + *quat[j] = (matrix[(j * 4u) + i] + matrix[(i * 4u) + j]) * root; + *quat[k] = (matrix[(k * 4u) + i] + matrix[(i * 4u) + k]) * root; + } + + return {translation, rotation, scale}; +} + +namespace iris +{ + +Transform::Transform() + : Transform({}, {}, {1.0f}) +{ +} + +Transform::Transform(const Matrix4 &matrix) + : Transform({0.0f}, {}, {0.0f}) +{ + const auto [translation, rotation, scale] = decompose(matrix); + + translation_ = translation; + rotation_ = rotation; + scale_ = scale; +} + +Transform::Transform(const Vector3 &translation, const Quaternion &rotation, const Vector3 &scale) + : translation_(translation) + , rotation_(rotation) + , scale_(scale) +{ +} + +Matrix4 Transform::matrix() const +{ + return Matrix4::make_translate(translation_) * Matrix4(rotation_) * Matrix4::make_scale(scale_); +} + +void Transform::set_matrix(const Matrix4 &matrix) +{ + const auto [translation, rotation, scale] = decompose(matrix); + + translation_ = translation; + rotation_ = rotation; + scale_ = scale; +} + +void Transform::interpolate(const Transform &other, float amount) +{ + translation_.lerp(other.translation_, amount); + rotation_.slerp(other.rotation_, amount); + scale_.lerp(other.scale_, amount); +} + +Vector3 Transform::translation() const +{ + return translation_; +} + +void Transform::set_translation(const Vector3 &translation) +{ + translation_ = translation; +} + +Quaternion Transform::rotation() const +{ + return rotation_; +} + +void Transform::set_rotation(const Quaternion &rotation) +{ + rotation_ = rotation; +} + +Vector3 Transform::scale() const +{ + return scale_; +} + +void Transform::set_scale(const Vector3 &scale) +{ + scale_ = scale; +} + +bool Transform::operator==(const Transform &other) const +{ + return (translation_ == other.translation_) && (rotation_ == other.rotation_) && (scale_ == other.scale_); +} + +bool Transform::operator!=(const Transform &other) const +{ + return !(*this == other); +} + +Transform Transform::operator*(const Transform &other) const +{ + return Transform{*this} *= other; +} + +Transform &Transform::operator*=(const Transform &other) +{ + return *this *= other.matrix(); +} + +Transform Transform::operator*(const Matrix4 &other) const +{ + return Transform{*this} *= other; +} + +Transform &Transform::operator*=(const Matrix4 &other) +{ + auto new_matrix = matrix() * other; + + const auto [translation, rotation, scale] = decompose(new_matrix); + + translation_ = translation; + rotation_ = rotation; + scale_ = scale; + + return *this; +} + +} diff --git a/src/core/utils.cpp b/src/core/utils.cpp new file mode 100644 index 00000000..124441f8 --- /dev/null +++ b/src/core/utils.cpp @@ -0,0 +1,32 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/utils.h" + +#include +#include +#include + +namespace iris +{ + +bool compare(float a, float b) +{ + static constexpr auto epsilon = std::numeric_limits::epsilon(); + + const auto diff = std::fabs(a - b); + a = std::fabs(a); + b = std::fabs(b); + + // find largest value + // use an upper of one to prevent our scaled epsilon getting too large + const auto largest = std::max({1.0f, a, b}); + + // compare using a relative epsilon + return diff <= (largest * epsilon); +} + +} diff --git a/src/core/win32/CMakeLists.txt b/src/core/win32/CMakeLists.txt new file mode 100644 index 00000000..b9a22255 --- /dev/null +++ b/src/core/win32/CMakeLists.txt @@ -0,0 +1,7 @@ +set(DEFAULT_ROOT "${PROJECT_SOURCE_DIR}/src/core/default") + +target_sources(iris PRIVATE + ${DEFAULT_ROOT}/resource_loader.cpp + semaphore.cpp + start.cpp + thread.cpp) diff --git a/src/core/win32/semaphore.cpp b/src/core/win32/semaphore.cpp new file mode 100644 index 00000000..af010146 --- /dev/null +++ b/src/core/win32/semaphore.cpp @@ -0,0 +1,50 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/semaphore.h" + +#include +#include +#include + +#include + +#include "core/auto_release.h" +#include "core/error_handling.h" + +namespace iris +{ + +struct Semaphore::implementation +{ + AutoRelease semaphore; +}; + +Semaphore::Semaphore(std::ptrdiff_t initial) + : impl_(std::make_unique()) +{ + impl_->semaphore = {::CreateSemaphoreA(NULL, static_cast(initial), 10000u, NULL), ::CloseHandle}; + + expect(impl_, "could not create semaphore"); +} + +Semaphore::~Semaphore() = default; +Semaphore::Semaphore(Semaphore &&) = default; +Semaphore &Semaphore::operator=(Semaphore &&) = default; + +void Semaphore::release() +{ + const auto release = ::ReleaseSemaphore(impl_->semaphore, 1, NULL); + expect(release != 0, "could not release semaphore"); +} + +void Semaphore::acquire() +{ + const auto wait = ::WaitForSingleObject(impl_->semaphore, INFINITE); + expect(wait != WAIT_FAILED, "could not acquire semaphore"); +} + +} diff --git a/src/core/win32/start.cpp b/src/core/win32/start.cpp new file mode 100644 index 00000000..b2ad754f --- /dev/null +++ b/src/core/win32/start.cpp @@ -0,0 +1,84 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/start.h" + +#include + +#include "core/root.h" +#include "graphics/d3d12/d3d12_mesh_manager.h" +#include "graphics/d3d12/d3d12_texture_manager.h" +#include "graphics/opengl/opengl_mesh_manager.h" +#include "graphics/opengl/opengl_texture_manager.h" +#include "graphics/win32/win32_window_manager.h" +#include "iris_version.h" +#include "jobs/fiber/fiber_job_system_manager.h" +#include "jobs/thread/thread_job_system_manager.h" +#include "log/log.h" +#include "log/logger.h" +#include "physics/bullet/bullet_physics_manager.h" + +namespace +{ + +void register_apis() +{ + iris::Root::register_graphics_api( + "d3d12", + std::make_unique(), + std::make_unique(), + std::make_unique()); + + iris::Root::register_graphics_api( + "opengl", + std::make_unique(), + std::make_unique(), + std::make_unique()); + + iris::Root::set_graphics_api("d3d12"); + + iris::Root::register_physics_api("bullet", std::make_unique()); + + iris::Root::set_physics_api("bullet"); + + iris::Root::register_jobs_api("thread", std::make_unique()); + + iris::Root::register_jobs_api("fiber", std::make_unique()); + + iris::Root::set_jobs_api("fiber"); +} + +} + +namespace iris +{ + +void start(int argc, char **argv, std::function entry) +{ + LOG_ENGINE_INFO("start", "engine start {}", IRIS_VERSION_STR); + + register_apis(); + + entry(argc, argv); + + Root::reset(); +} + +void start_debug(int argc, char **argv, std::function entry) +{ + // enable engine logging + Logger::instance().set_log_engine(true); + + LOG_ENGINE_INFO("start", "engine start (with debugging) {}", IRIS_VERSION_STR); + + register_apis(); + + entry(argc, argv); + + Root::reset(); +} + +} diff --git a/src/core/win32/thread.cpp b/src/core/win32/thread.cpp new file mode 100644 index 00000000..c082fdff --- /dev/null +++ b/src/core/win32/thread.cpp @@ -0,0 +1,45 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/thread.h" + +#include + +#include + +#include "core/error_handling.h" + +namespace iris +{ + +Thread::Thread() + : thread_() +{ +} + +bool Thread::joinable() const +{ + return thread_.joinable(); +} + +void Thread::join() +{ + thread_.join(); +} + +std::thread::id Thread::get_id() const +{ + return thread_.get_id(); +} + +void Thread::bind_to_core(std::size_t core) +{ + DWORD affinity_mask = (1u << core); + + expect(::SetThreadAffinityMask(thread_.native_handle(), affinity_mask) != 0u, "could not bind thread to core"); +} + +} diff --git a/src/events/CMakeLists.txt b/src/events/CMakeLists.txt new file mode 100644 index 00000000..24936827 --- /dev/null +++ b/src/events/CMakeLists.txt @@ -0,0 +1,10 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/events") + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/event.h + ${INCLUDE_ROOT}/event_type.h + ${INCLUDE_ROOT}/keyboard_event.h + ${INCLUDE_ROOT}/mouse_event.h + ${INCLUDE_ROOT}/quit_event.h + ${INCLUDE_ROOT}/touch_event.h + event.cpp) diff --git a/src/events/event.cpp b/src/events/event.cpp new file mode 100644 index 00000000..578c1946 --- /dev/null +++ b/src/events/event.cpp @@ -0,0 +1,167 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "events/event.h" + +#include "events/event_type.h" +#include "events/keyboard_event.h" +#include "events/mouse_button_event.h" +#include "events/mouse_event.h" +#include "events/quit_event.h" +#include "events/touch_event.h" + +namespace iris +{ + +Event::Event(QuitEvent event) + : type_(EventType::QUIT) + , event_(event) +{ +} + +Event::Event(const KeyboardEvent event) + : type_(EventType::KEYBOARD) + , event_(event) +{ +} + +Event::Event(const MouseEvent event) + : type_(EventType::MOUSE) + , event_(event) +{ +} + +Event::Event(MouseButtonEvent event) + : type_(EventType::MOUSE_BUTTON) + , event_(event) +{ +} + +Event::Event(TouchEvent event) + : type_(EventType::TOUCH) + , event_(event) +{ +} + +EventType Event::type() const +{ + return type_; +} + +bool Event::is_quit() const +{ + return std::holds_alternative(event_); +} + +bool Event::is_key() const +{ + return std::holds_alternative(event_); +} + +bool Event::is_key(Key key) const +{ + auto match = false; + + if (auto val = std::get_if(&event_); val) + { + match = val->key == key; + } + + return match; +} + +bool Event::is_key(Key key, KeyState state) const +{ + auto match = false; + + if (auto val = std::get_if(&event_); val) + { + match = (val->key) == key && (val->state == state); + } + + return match; +} + +KeyboardEvent Event::key() const +{ + if (!is_key()) + { + throw Exception("not keyboard event"); + } + + return std::get(event_); +} + +bool Event::is_mouse() const +{ + return std::holds_alternative(event_); +} + +MouseEvent Event::mouse() const +{ + if (!is_mouse()) + { + throw Exception("not mouse event"); + } + + return std::get(event_); +} + +bool Event::is_mouse_button() const +{ + return std::holds_alternative(event_); +} + +bool Event::is_mouse_button(MouseButton button) const +{ + auto match = false; + + if (auto val = std::get_if(&event_); val) + { + match = val->button == button; + } + + return match; +} + +bool Event::is_mouse_button(MouseButton button, MouseButtonState state) const +{ + auto match = false; + + if (auto val = std::get_if(&event_); val) + { + match = (val->button == button) && (val->state == state); + } + + return match; +} + +MouseButtonEvent Event::mouse_button() const +{ + if (!is_key()) + { + throw Exception("not mouse button event"); + } + + return std::get(event_); +} + +bool Event::is_touch() const +{ + return std::holds_alternative(event_); +} + +TouchEvent Event::touch() const +{ + if (!is_touch()) + { + throw Exception("not touch event"); + } + + return std::get(event_); +} + +} diff --git a/src/graphics/CMakeLists.txt b/src/graphics/CMakeLists.txt new file mode 100644 index 00000000..b65659b5 --- /dev/null +++ b/src/graphics/CMakeLists.txt @@ -0,0 +1,61 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/graphics") + +if(IRIS_PLATFORM MATCHES "MACOS") + add_subdirectory("macos") + add_subdirectory("metal") + add_subdirectory("opengl") +elseif(IRIS_PLATFORM MATCHES "IOS") + add_subdirectory("ios") + add_subdirectory("metal") +elseif(IRIS_PLATFORM MATCHES "WIN32") + add_subdirectory("win32") + add_subdirectory("opengl") + add_subdirectory("d3d12") +endif() + +add_subdirectory("lights") +add_subdirectory("render_graph") + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/animation.h + ${INCLUDE_ROOT}/bone.h + ${INCLUDE_ROOT}/keyframe.h + ${INCLUDE_ROOT}/material.h + ${INCLUDE_ROOT}/mesh.h + ${INCLUDE_ROOT}/mesh_loader.h + ${INCLUDE_ROOT}/mesh_manager.h + ${INCLUDE_ROOT}/texture_usage.h + ${INCLUDE_ROOT}/primitive_type.h + ${INCLUDE_ROOT}/render_command_type.h + ${INCLUDE_ROOT}/render_command.h + ${INCLUDE_ROOT}/render_entity.h + ${INCLUDE_ROOT}/render_pass.h + ${INCLUDE_ROOT}/render_queue_builder.h + ${INCLUDE_ROOT}/render_target.h + ${INCLUDE_ROOT}/renderer.h + ${INCLUDE_ROOT}/scene.h + ${INCLUDE_ROOT}/skeleton.h + ${INCLUDE_ROOT}/text_factory.h + ${INCLUDE_ROOT}/texture.h + ${INCLUDE_ROOT}/texture_manager.h + ${INCLUDE_ROOT}/vertex_attributes.h + ${INCLUDE_ROOT}/vertex_data.h + ${INCLUDE_ROOT}/weight.h + ${INCLUDE_ROOT}/window_manager.h + ${INCLUDE_ROOT}/window.h + animation.cpp + bone.cpp + mesh.cpp + mesh_loader.cpp + mesh_manager.cpp + render_command.cpp + render_entity.cpp + render_queue_builder.cpp + render_target.cpp + renderer.cpp + scene.cpp + skeleton.cpp + texture.cpp + texture_manager.cpp + vertex_attributes.cpp + window.cpp) diff --git a/src/graphics/animation.cpp b/src/graphics/animation.cpp new file mode 100644 index 00000000..23b1d6a4 --- /dev/null +++ b/src/graphics/animation.cpp @@ -0,0 +1,99 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/animation.h" + +#include +#include +#include + +#include "core/error_handling.h" +#include "core/matrix4.h" +#include "core/quaternion.h" +#include "core/vector3.h" +#include "log/log.h" + +using namespace std::chrono_literals; + +namespace iris +{ + +Animation::Animation( + std::chrono::milliseconds duration, + const std::string &name, + const std::map> &frames) + : time_(0ms) + , last_advance_(std::chrono::steady_clock::now()) + , duration_(duration) + , name_(name) + , frames_(frames) +{ +} + +std::string Animation::name() const +{ + return name_; +} + +Transform Animation::transform(const std::string &bone) const +{ + expect(bone_exists(bone), "no animation for bone"); + + const auto &keyframes = frames_.at(bone); + + // find the first keyframe *after* current time + const auto &second_keyframe = std::find_if( + std::cbegin(keyframes) + 1u, std::cend(keyframes), [this](const KeyFrame &kf) { return kf.time >= time_; }); + + expect(second_keyframe != std::cend(keyframes), "cannot find keyframe"); + + const auto first_keyframe = second_keyframe - 1u; + + // calculate interpolation amount + const auto delta1 = second_keyframe->time - first_keyframe->time; + const auto delta2 = time_ - first_keyframe->time; + const auto interpolation = static_cast(delta2.count()) / static_cast(delta1.count()); + + // interpolate between frames + auto transform = first_keyframe->transform; + transform.interpolate(second_keyframe->transform, interpolation); + + return transform; +} + +bool Animation::bone_exists(const std::string &bone) const +{ + return frames_.count(bone) != 0u; +} + +void Animation::advance() +{ + const auto now = std::chrono::steady_clock::now(); + const auto delta = std::chrono::duration_cast(now - last_advance_); + + // update time, ensuring we wrap around so animation loops + time_ = (time_ + delta) % duration_; + + last_advance_ = now; +} + +void Animation::reset() +{ + time_ = 0ms; + last_advance_ = std::chrono::steady_clock::now(); +} + +std::chrono::milliseconds Animation::duration() const +{ + return duration_; +} + +void Animation::set_time(std::chrono::milliseconds time) +{ + time_ = time; +} + +} diff --git a/src/graphics/bone.cpp b/src/graphics/bone.cpp new file mode 100644 index 00000000..ff4956f4 --- /dev/null +++ b/src/graphics/bone.cpp @@ -0,0 +1,76 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/bone.h" + +#include +#include +#include +#include + +#include "core/matrix4.h" +#include "core/transform.h" +#include "graphics/weight.h" + +namespace iris +{ + +Bone::Bone( + const std::string &name, + const std::string &parent, + const std::vector &weights, + const Matrix4 &offset, + const Matrix4 &transform) + : name_(name) + , parent_(parent) + , weights_(weights) + , offset_(offset) + , transform_(transform) + , is_manual_(false) +{ +} + +std::string Bone::name() const +{ + return name_; +} + +const std::vector &Bone::weights() const +{ + return weights_; +} + +const Matrix4 &Bone::offset() const +{ + return offset_; +} + +std::string Bone::parent() const +{ + return parent_; +} + +const Matrix4 &Bone::transform() const +{ + return transform_; +} + +void Bone::set_transform(const Matrix4 &transform) +{ + transform_ = transform; +} + +bool Bone::is_manual() const +{ + return is_manual_; +} + +void Bone::set_manual(bool is_manual) +{ + is_manual_ = is_manual; +} + +} diff --git a/src/graphics/d3d12/CMakeLists.txt b/src/graphics/d3d12/CMakeLists.txt new file mode 100644 index 00000000..0bd87f13 --- /dev/null +++ b/src/graphics/d3d12/CMakeLists.txt @@ -0,0 +1,35 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/graphics/d3d12") + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/d3d12_buffer.h + ${INCLUDE_ROOT}/d3d12_constant_buffer.h + ${INCLUDE_ROOT}/d3d12_constant_buffer_pool.h + ${INCLUDE_ROOT}/d3d12_context.h + ${INCLUDE_ROOT}/d3d12_cpu_descriptor_handle_allocator.h + ${INCLUDE_ROOT}/d3d12_descriptor_handle.h + ${INCLUDE_ROOT}/d3d12_descriptor_manager.h + ${INCLUDE_ROOT}/d3d12_gpu_descriptor_handle_allocator.h + ${INCLUDE_ROOT}/d3d12_material.h + ${INCLUDE_ROOT}/d3d12_mesh.h + ${INCLUDE_ROOT}/d3d12_mesh_manager.h + ${INCLUDE_ROOT}/d3d12_render_target.h + ${INCLUDE_ROOT}/d3d12_renderer.h + ${INCLUDE_ROOT}/d3d12_texture.h + ${INCLUDE_ROOT}/d3d12_texture_manager.h + ${INCLUDE_ROOT}/hlsl_shader_compiler.h + d3d12_buffer.cpp + d3d12_constant_buffer.cpp + d3d12_constant_buffer_pool.cpp + d3d12_context.cpp + d3d12_cpu_descriptor_handle_allocator.cpp + d3d12_descriptor_handle.cpp + d3d12_descriptor_manager.cpp + d3d12_gpu_descriptor_handle_allocator.cpp + d3d12_material.cpp + d3d12_mesh.cpp + d3d12_mesh_manager.cpp + d3d12_render_target.cpp + d3d12_renderer.cpp + d3d12_texture.cpp + d3d12_texture_manager.cpp + hlsl_shader_compiler.cpp) diff --git a/src/graphics/d3d12/d3d12_buffer.cpp b/src/graphics/d3d12/d3d12_buffer.cpp new file mode 100644 index 00000000..c51a4324 --- /dev/null +++ b/src/graphics/d3d12/d3d12_buffer.cpp @@ -0,0 +1,152 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/d3d12/d3d12_buffer.h" + +#include +#include + +#include + +#include "directx/d3d12.h" + +#include "core/data_buffer.h" +#include "core/error_handling.h" +#include "graphics/d3d12/d3d12_context.h" +#include "graphics/vertex_data.h" + +namespace +{ + +/** + * Helper function to create a new GPU D3D12 buffer. + * + * @param size + * Size of buffer (in bytes). + * + * @param mapped_memory + * Out pointer (to a pointer) where the buffer will be mapped to the CPU. + */ +Microsoft::WRL::ComPtr create_resource(std::size_t size, std::byte **mapped_memory) +{ + auto *device = iris::D3D12Context::device(); + + // create the buffer on the upload heap + CD3DX12_HEAP_PROPERTIES heap_properties(D3D12_HEAP_TYPE_UPLOAD); + const auto buffer_descriptor = CD3DX12_RESOURCE_DESC::Buffer(static_cast(size)); + Microsoft::WRL::ComPtr resource = nullptr; + + const auto commit_resource = device->CreateCommittedResource( + &heap_properties, + D3D12_HEAP_FLAG_NONE, + &buffer_descriptor, + D3D12_RESOURCE_STATE_GENERIC_READ, + nullptr, + IID_PPV_ARGS(&resource)); + iris::expect(commit_resource == S_OK, "could not create committed resource"); + + CD3DX12_RANGE read_range(0, 0); + + // map the gpu buffer to the cpu, so it can be written to + iris::expect( + resource->Map(0u, &read_range, reinterpret_cast(mapped_memory)) == S_OK, "could not map buffer"); + + return resource; +} + +} + +namespace iris +{ + +D3D12Buffer::D3D12Buffer(const std::vector &vertex_data) + : resource_() + , vertex_buffer_view_() + , index_buffer_view_() + , element_count_(vertex_data.size()) + , capacity_(element_count_) + , mapped_memory_(nullptr) +{ + resource_ = create_resource(capacity_ * sizeof(VertexData), &mapped_memory_); + + write(vertex_data); + + vertex_buffer_view_.BufferLocation = resource_->GetGPUVirtualAddress(); + vertex_buffer_view_.SizeInBytes = static_cast(element_count_ * sizeof(VertexData)); + vertex_buffer_view_.StrideInBytes = sizeof(VertexData); +} + +D3D12Buffer::D3D12Buffer(const std::vector &index_data) + : resource_() + , vertex_buffer_view_() + , index_buffer_view_() + , element_count_(index_data.size()) + , capacity_(element_count_) + , mapped_memory_(nullptr) +{ + resource_ = create_resource(capacity_ * sizeof(std::uint32_t), &mapped_memory_); + + write(index_data); + + index_buffer_view_.BufferLocation = resource_->GetGPUVirtualAddress(); + index_buffer_view_.SizeInBytes = static_cast(element_count_ * sizeof(std::uint32_t)); + index_buffer_view_.Format = DXGI_FORMAT_R32_UINT; +} + +std::size_t D3D12Buffer::element_count() const +{ + return element_count_; +} + +D3D12_VERTEX_BUFFER_VIEW D3D12Buffer::vertex_view() const +{ + return vertex_buffer_view_; +} + +D3D12_INDEX_BUFFER_VIEW D3D12Buffer::index_view() const +{ + return index_buffer_view_; +} + +void D3D12Buffer::write(const std::vector &vertex_data) +{ + element_count_ = vertex_data.size(); + + // if buffer is too small for new data then reallocate + if (element_count_ > capacity_) + { + capacity_ = element_count_; + resource_ = create_resource(capacity_ * sizeof(VertexData), &mapped_memory_); + + vertex_buffer_view_.BufferLocation = resource_->GetGPUVirtualAddress(); + vertex_buffer_view_.SizeInBytes = static_cast(capacity_ * sizeof(VertexData)); + vertex_buffer_view_.StrideInBytes = sizeof(VertexData); + } + + // copy new data + std::memcpy(mapped_memory_, vertex_data.data(), element_count_ * sizeof(VertexData)); +} + +void D3D12Buffer::write(const std::vector &index_data) +{ + element_count_ = index_data.size(); + + // if buffer is too small for new data then reallocate + if (element_count_ > capacity_) + { + capacity_ = element_count_; + resource_ = create_resource(capacity_ * sizeof(std::uint32_t), &mapped_memory_); + + index_buffer_view_.BufferLocation = resource_->GetGPUVirtualAddress(); + index_buffer_view_.SizeInBytes = static_cast(capacity_ * sizeof(std::uint32_t)); + index_buffer_view_.Format = DXGI_FORMAT_R32_UINT; + } + + // copy new data + std::memcpy(mapped_memory_, index_data.data(), element_count_ * sizeof(std::uint32_t)); +} + +} diff --git a/src/graphics/d3d12/d3d12_constant_buffer.cpp b/src/graphics/d3d12/d3d12_constant_buffer.cpp new file mode 100644 index 00000000..d641427c --- /dev/null +++ b/src/graphics/d3d12/d3d12_constant_buffer.cpp @@ -0,0 +1,104 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/d3d12/d3d12_constant_buffer.h" + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +#include "core/error_handling.h" +#include "graphics/d3d12/d3d12_context.h" +#include "graphics/d3d12/d3d12_cpu_descriptor_handle_allocator.h" +#include "graphics/d3d12/d3d12_descriptor_handle.h" +#include "graphics/d3d12/d3d12_descriptor_manager.h" +#include "graphics/d3d12/d3d12_gpu_descriptor_handle_allocator.h" +#include "graphics/texture_manager.h" + +namespace +{ + +/** + * Helper function to create a D3D12 buffer on the upload heap. + * + * @param capacity + * Size of buffer (in bytes). + * + * @param resource + * A D3D12 handle to be set to the new resource. + * + * @returns + * Descriptor handle to buffer. + */ +iris::D3D12DescriptorHandle create_resource(std::size_t capacity, Microsoft::WRL::ComPtr &resource) +{ + const auto upload_heap = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD); + const auto heap_descriptor = CD3DX12_RESOURCE_DESC::Buffer(capacity); + + auto *device = iris::D3D12Context::device(); + + // create the buffer + const auto commit_resource = device->CreateCommittedResource( + &upload_heap, + D3D12_HEAP_FLAG_NONE, + &heap_descriptor, + D3D12_RESOURCE_STATE_GENERIC_READ, + nullptr, + IID_PPV_ARGS(&resource)); + iris::expect(commit_resource == S_OK, "could not create constant buffer"); + + // allocate descriptor for buffer + return iris::D3D12DescriptorManager::cpu_allocator(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV).allocate_dynamic(); +} + +} + +namespace iris +{ + +D3D12ConstantBuffer::D3D12ConstantBuffer() + : capacity_(1u) + , mapped_buffer_(nullptr) + , resource_(nullptr) + , descriptor_handle_(create_resource(capacity_, resource_)) +{ + auto *device = iris::D3D12Context::device(); + + device->CreateConstantBufferView(nullptr, descriptor_handle_.cpu_handle()); + + // note we don't create a view or map the resource, it's a null buffer so + // there are no valid actions on it +} + +D3D12ConstantBuffer::D3D12ConstantBuffer(std::uint32_t capacity) + : capacity_(capacity) + , mapped_buffer_(nullptr) + , resource_(nullptr) + , descriptor_handle_(create_resource(capacity_, resource_)) +{ + auto *device = D3D12Context::device(); + + D3D12_CONSTANT_BUFFER_VIEW_DESC cbv_descriptor = {0}; + cbv_descriptor.BufferLocation = resource_->GetGPUVirtualAddress(); + cbv_descriptor.SizeInBytes = capacity_; + + // create a view onto the buffer + device->CreateConstantBufferView(&cbv_descriptor, descriptor_handle_.cpu_handle()); + + // map the buffer to the cpu so we can write to it + const auto map_resource = resource_->Map(0u, NULL, reinterpret_cast(&mapped_buffer_)); + expect(map_resource == S_OK, "failed to map constant buffer"); +} + +D3D12DescriptorHandle D3D12ConstantBuffer::descriptor_handle() const +{ + return descriptor_handle_; +} + +} diff --git a/src/graphics/d3d12/d3d12_constant_buffer_pool.cpp b/src/graphics/d3d12/d3d12_constant_buffer_pool.cpp new file mode 100644 index 00000000..650f8538 --- /dev/null +++ b/src/graphics/d3d12/d3d12_constant_buffer_pool.cpp @@ -0,0 +1,27 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/d3d12/d3d12_constant_buffer_pool.h" + +namespace iris +{ + +D3D12ConstantBufferPool::D3D12ConstantBufferPool() + : buffers_() + , index_(0u) +{ + for (auto i = 0u; i < 3; ++i) + { + buffers_.emplace_back(7168u); + } +} + +D3D12ConstantBuffer &D3D12ConstantBufferPool::next() +{ + return buffers_[index_++ % buffers_.size()]; +} + +} diff --git a/src/graphics/d3d12/d3d12_context.cpp b/src/graphics/d3d12/d3d12_context.cpp new file mode 100644 index 00000000..09dc2be0 --- /dev/null +++ b/src/graphics/d3d12/d3d12_context.cpp @@ -0,0 +1,184 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/d3d12/d3d12_context.h" + +#include +#include + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +#include "core/error_handling.h" + +namespace iris +{ + +D3D12Context::D3D12Context() + : dxgi_factory_(nullptr) + , device_(nullptr) + , info_queue_(nullptr) + , root_signature_(nullptr) + , num_descriptors_(0u) +{ + // create and enable a debug layer + Microsoft::WRL::ComPtr debug_interface = nullptr; + expect(::D3D12GetDebugInterface(IID_PPV_ARGS(&debug_interface)) == S_OK, "could not create debug interface"); + + debug_interface->EnableDebugLayer(); + + expect( + ::CreateDXGIFactory2(DXGI_CREATE_FACTORY_DEBUG, IID_PPV_ARGS(&dxgi_factory_)) == S_OK, + "could not create dxgi factory"); + + Microsoft::WRL::ComPtr dxgi_adaptor_tmp = nullptr; + Microsoft::WRL::ComPtr dxgi_adaptor = nullptr; + SIZE_T max_dedicated_memory = 0u; + UINT32 index = 0u; + + // search all adaptors for one that can be used to create a d3d12 device + // (with the most amount of dedicated video memory) + while (dxgi_factory_->EnumAdapters1(index++, &dxgi_adaptor_tmp) != DXGI_ERROR_NOT_FOUND) + { + DXGI_ADAPTER_DESC1 adaptor_descriptor = {0}; + dxgi_adaptor_tmp->GetDesc1(&adaptor_descriptor); + + // ignore the software renderer + if ((adaptor_descriptor.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) == 0) + { + // test if we can create a d3d12 device with this adaptor + if (::D3D12CreateDevice(dxgi_adaptor_tmp.Get(), D3D_FEATURE_LEVEL_11_0, __uuidof(ID3D12Device), nullptr) == + S_FALSE) + { + // check if this adaptor has more available memory + if (adaptor_descriptor.DedicatedVideoMemory > max_dedicated_memory) + { + const auto cast = dxgi_adaptor_tmp.As(&dxgi_adaptor); + expect(cast == S_OK, "failed to cast dxgi adaptor"); + + max_dedicated_memory = adaptor_descriptor.DedicatedVideoMemory; + } + } + } + } + + ensure(dxgi_adaptor != nullptr, "could not find a directx12 adapter"); + + // create actual d3d12 device + expect( + ::D3D12CreateDevice(dxgi_adaptor.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&device_)) == S_OK, + "could not create directx12 device"); + + expect(device_.As(&info_queue_) == S_OK, "could not cast device to info queue"); + + // set break on error and warning + info_queue_->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_CORRUPTION, TRUE); + info_queue_->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_ERROR, TRUE); + info_queue_->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_WARNING, TRUE); + + CD3DX12_DESCRIPTOR_RANGE1 ranges[2]; + CD3DX12_ROOT_PARAMETER1 root_parameters[2]; + + // see D3D12Renderer source for usage + static const auto num_cbv_descriptors = 2u; + static const auto num_srv_descriptors = 5u; + num_descriptors_ = num_cbv_descriptors + num_srv_descriptors; + + // setup root signature + + ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, num_cbv_descriptors, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC); + ranges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, num_srv_descriptors, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC); + + root_parameters[0].InitAsDescriptorTable(2, &ranges[0], D3D12_SHADER_VISIBILITY_VERTEX); + root_parameters[1].InitAsDescriptorTable(2, &ranges[0], D3D12_SHADER_VISIBILITY_PIXEL); + + D3D12_ROOT_SIGNATURE_FLAGS root_signature_flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT | + D3D12_ROOT_SIGNATURE_FLAG_DENY_HULL_SHADER_ROOT_ACCESS | + D3D12_ROOT_SIGNATURE_FLAG_DENY_DOMAIN_SHADER_ROOT_ACCESS | + D3D12_ROOT_SIGNATURE_FLAG_DENY_GEOMETRY_SHADER_ROOT_ACCESS; + + // create a sampler to store in the root signature + D3D12_STATIC_SAMPLER_DESC sampler = {}; + sampler.Filter = D3D12_FILTER_MIN_MAG_MIP_POINT; + sampler.AddressU = D3D12_TEXTURE_ADDRESS_MODE_BORDER; + sampler.AddressV = D3D12_TEXTURE_ADDRESS_MODE_BORDER; + sampler.AddressW = D3D12_TEXTURE_ADDRESS_MODE_BORDER; + sampler.MipLODBias = 0; + sampler.MaxAnisotropy = 0; + sampler.ComparisonFunc = D3D12_COMPARISON_FUNC_LESS; + sampler.BorderColor = D3D12_STATIC_BORDER_COLOR_OPAQUE_WHITE; + sampler.MinLOD = 0.0f; + sampler.MaxLOD = D3D12_FLOAT32_MAX; + sampler.ShaderRegister = 0; + sampler.RegisterSpace = 0; + sampler.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + + CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC root_signature_description{}; + root_signature_description.Init_1_1(_countof(root_parameters), root_parameters, 1u, &sampler, root_signature_flags); + + Microsoft::WRL::ComPtr signature = nullptr; + Microsoft::WRL::ComPtr error = nullptr; + if (::D3DX12SerializeVersionedRootSignature( + &root_signature_description, D3D_ROOT_SIGNATURE_VERSION_1, &signature, &error) != S_OK) + { + const std::string error_message(static_cast(error->GetBufferPointer()), error->GetBufferSize()); + + throw iris::Exception("root signature serialization failed: " + error_message); + } + + expect( + device_->CreateRootSignature( + 0, signature->GetBufferPointer(), signature->GetBufferSize(), IID_PPV_ARGS(&root_signature_)) == S_OK, + "could not create root signature"); +} + +D3D12Context &D3D12Context::instance() +{ + static D3D12Context instance{}; + return instance; +} + +IDXGIFactory4 *D3D12Context::dxgi_factory() +{ + return instance().dxgi_factory_impl(); +} + +ID3D12Device2 *D3D12Context::device() +{ + return instance().device_impl(); +} + +ID3D12RootSignature *D3D12Context::root_signature() +{ + return instance().root_signature_impl(); +} + +std::uint32_t D3D12Context::num_descriptors() +{ + return instance().num_descriptors_impl(); +} + +IDXGIFactory4 *D3D12Context::dxgi_factory_impl() const +{ + return dxgi_factory_.Get(); +} + +ID3D12Device2 *D3D12Context::device_impl() const +{ + return device_.Get(); +} + +ID3D12RootSignature *D3D12Context::root_signature_impl() const +{ + return root_signature_.Get(); +} + +std::uint32_t D3D12Context::num_descriptors_impl() const +{ + return num_descriptors_; +} + +} diff --git a/src/graphics/d3d12/d3d12_cpu_descriptor_handle_allocator.cpp b/src/graphics/d3d12/d3d12_cpu_descriptor_handle_allocator.cpp new file mode 100644 index 00000000..bd410c4d --- /dev/null +++ b/src/graphics/d3d12/d3d12_cpu_descriptor_handle_allocator.cpp @@ -0,0 +1,93 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/d3d12/d3d12_cpu_descriptor_handle_allocator.h" + +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +#include "core/error_handling.h" +#include "graphics/d3d12/d3d12_context.h" +#include "graphics/d3d12/d3d12_descriptor_handle.h" + +namespace iris +{ + +D3D12CPUDescriptorHandleAllocator::D3D12CPUDescriptorHandleAllocator( + D3D12_DESCRIPTOR_HEAP_TYPE type, + std::uint32_t num_descriptors, + std::uint32_t static_size, + bool shader_visible) + : descriptor_heap_(nullptr) + , heap_start_() + , descriptor_size_(0u) + , static_index_(0u) + , dynamic_index_(static_size) + , static_capacity_(static_size) + , dynamic_capacity_(num_descriptors - static_size) +{ + auto *device = D3D12Context::device(); + + // setup heap description + D3D12_DESCRIPTOR_HEAP_DESC heap_description; + heap_description.NumDescriptors = num_descriptors; + heap_description.Type = type; + heap_description.Flags = + shader_visible ? D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE : D3D12_DESCRIPTOR_HEAP_FLAG_NONE; + heap_description.NodeMask = 0; + + // create heap + expect( + device->CreateDescriptorHeap(&heap_description, IID_PPV_ARGS(&descriptor_heap_)) == S_OK, + "could not create descriptor heap"); + + descriptor_size_ = device->GetDescriptorHandleIncrementSize(type); + + heap_start_ = descriptor_heap_->GetCPUDescriptorHandleForHeapStart(); +} + +D3D12DescriptorHandle D3D12CPUDescriptorHandleAllocator::allocate_static() +{ + expect(static_index_ < static_capacity_, "heap too small"); + + // get next free descriptor form static pool + D3D12_CPU_DESCRIPTOR_HANDLE cpu_handle = heap_start_; + cpu_handle.ptr += descriptor_size_ * static_index_; + + ++static_index_; + + return {cpu_handle}; +} + +D3D12DescriptorHandle D3D12CPUDescriptorHandleAllocator::allocate_dynamic() +{ + expect(dynamic_index_ < dynamic_capacity_, "heap too small"); + + // get next free descriptor form dynamic pool + D3D12_CPU_DESCRIPTOR_HANDLE cpu_handle = heap_start_; + cpu_handle.ptr += descriptor_size_ * (static_capacity_ + dynamic_index_); + + ++dynamic_index_; + + return {cpu_handle}; +} + +std::uint32_t D3D12CPUDescriptorHandleAllocator::descriptor_size() const +{ + return descriptor_size_; +} + +void D3D12CPUDescriptorHandleAllocator::reset_dynamic() +{ + dynamic_index_ = 0u; +} +} diff --git a/src/graphics/d3d12/d3d12_descriptor_handle.cpp b/src/graphics/d3d12/d3d12_descriptor_handle.cpp new file mode 100644 index 00000000..aecb8be6 --- /dev/null +++ b/src/graphics/d3d12/d3d12_descriptor_handle.cpp @@ -0,0 +1,55 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/d3d12/d3d12_descriptor_handle.h" + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +namespace iris +{ + +D3D12DescriptorHandle::D3D12DescriptorHandle() + : cpu_handle_() + , gpu_handle_() +{ + cpu_handle_.ptr = NULL; + gpu_handle_.ptr = NULL; +} + +D3D12DescriptorHandle::D3D12DescriptorHandle(D3D12_CPU_DESCRIPTOR_HANDLE cpu_handle) + : cpu_handle_(cpu_handle) + , gpu_handle_() +{ + gpu_handle_.ptr = NULL; +} + +D3D12DescriptorHandle::D3D12DescriptorHandle( + D3D12_CPU_DESCRIPTOR_HANDLE cpu_handle, + D3D12_GPU_DESCRIPTOR_HANDLE gpu_handle) + : cpu_handle_(cpu_handle) + , gpu_handle_(gpu_handle) +{ +} + +D3D12_CPU_DESCRIPTOR_HANDLE D3D12DescriptorHandle::cpu_handle() const +{ + return cpu_handle_; +} + +D3D12_GPU_DESCRIPTOR_HANDLE D3D12DescriptorHandle::gpu_handle() const +{ + return gpu_handle_; +} + +D3D12DescriptorHandle::operator bool() const +{ + // only need to check cpu handle as there is no situation where we could + // have a gpu handle without a cpu handle + return cpu_handle_.ptr != NULL; +} + +} \ No newline at end of file diff --git a/src/graphics/d3d12/d3d12_descriptor_manager.cpp b/src/graphics/d3d12/d3d12_descriptor_manager.cpp new file mode 100644 index 00000000..0229d4d7 --- /dev/null +++ b/src/graphics/d3d12/d3d12_descriptor_manager.cpp @@ -0,0 +1,55 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/d3d12/d3d12_descriptor_manager.h" + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +#include "graphics/d3d12/d3d12_cpu_descriptor_handle_allocator.h" +#include "graphics/d3d12/d3d12_gpu_descriptor_handle_allocator.h" + +namespace iris +{ + +D3D12CPUDescriptorHandleAllocator &D3D12DescriptorManager::cpu_allocator(D3D12_DESCRIPTOR_HEAP_TYPE type) +{ + return instance().cpu_allocator_impl(type); +} + +D3D12GPUDescriptorHandleAllocator &D3D12DescriptorManager::gpu_allocator(D3D12_DESCRIPTOR_HEAP_TYPE type) +{ + return instance().gpu_allocator_impl(type); +} + +D3D12DescriptorManager::D3D12DescriptorManager() + : cpu_allocators_() + , gpu_allocators_() +{ + cpu_allocators_.insert( + {D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, {D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, 2048u, 100u}}); + cpu_allocators_.insert({D3D12_DESCRIPTOR_HEAP_TYPE_RTV, {D3D12_DESCRIPTOR_HEAP_TYPE_RTV, 1024u, 100u}}); + cpu_allocators_.insert({D3D12_DESCRIPTOR_HEAP_TYPE_DSV, {D3D12_DESCRIPTOR_HEAP_TYPE_DSV, 1024u, 100u}}); + gpu_allocators_.insert({D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, {D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV, 10240u}}); +} + +D3D12DescriptorManager &D3D12DescriptorManager::instance() +{ + static D3D12DescriptorManager dm{}; + return dm; +} + +D3D12CPUDescriptorHandleAllocator &D3D12DescriptorManager::cpu_allocator_impl(D3D12_DESCRIPTOR_HEAP_TYPE type) +{ + return cpu_allocators_.at(type); +} + +D3D12GPUDescriptorHandleAllocator &D3D12DescriptorManager::gpu_allocator_impl(D3D12_DESCRIPTOR_HEAP_TYPE type) +{ + return gpu_allocators_.at(type); +} + +} diff --git a/src/graphics/d3d12/d3d12_gpu_descriptor_handle_allocator.cpp b/src/graphics/d3d12/d3d12_gpu_descriptor_handle_allocator.cpp new file mode 100644 index 00000000..784f102e --- /dev/null +++ b/src/graphics/d3d12/d3d12_gpu_descriptor_handle_allocator.cpp @@ -0,0 +1,95 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/d3d12/d3d12_gpu_descriptor_handle_allocator.h" + +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +#include "core/error_handling.h" +#include "graphics/d3d12/d3d12_context.h" +#include "graphics/d3d12/d3d12_descriptor_handle.h" + +namespace iris +{ + +D3D12GPUDescriptorHandleAllocator::D3D12GPUDescriptorHandleAllocator( + D3D12_DESCRIPTOR_HEAP_TYPE type, + std::uint32_t num_descriptors) + : descriptor_heap_(nullptr) + , cpu_start_() + , gpu_start_() + , descriptor_size_(0u) + , index_(0u) + , capacity_(num_descriptors) +{ + auto *device = D3D12Context::device(); + + // setup heap description + D3D12_DESCRIPTOR_HEAP_DESC heap_description; + heap_description.NumDescriptors = num_descriptors; + heap_description.Type = type; + heap_description.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; + heap_description.NodeMask = 0; + + // create heap + expect( + device->CreateDescriptorHeap(&heap_description, IID_PPV_ARGS(&descriptor_heap_)) == S_OK, + "could not create descriptor heap"); + + cpu_start_ = descriptor_heap_->GetCPUDescriptorHandleForHeapStart(); + gpu_start_ = descriptor_heap_->GetGPUDescriptorHandleForHeapStart(); + + descriptor_size_ = device->GetDescriptorHandleIncrementSize(type); +} + +D3D12DescriptorHandle D3D12GPUDescriptorHandleAllocator::allocate(std::uint32_t count) +{ + expect(index_ + count <= capacity_, "heap too small"); + + auto cpu_handle = cpu_start_; + cpu_handle.ptr += descriptor_size_ * index_; + + auto gpu_handle = gpu_start_; + gpu_handle.ptr += descriptor_size_ * index_; + + index_ += count; + + return {cpu_handle, gpu_handle}; +} + +D3D12_CPU_DESCRIPTOR_HANDLE D3D12GPUDescriptorHandleAllocator::cpu_start() const +{ + return cpu_start_; +} + +D3D12_GPU_DESCRIPTOR_HANDLE D3D12GPUDescriptorHandleAllocator::gpu_start() const +{ + return gpu_start_; +} + +std::uint32_t D3D12GPUDescriptorHandleAllocator::descriptor_size() const +{ + return descriptor_size_; +} + +ID3D12DescriptorHeap *D3D12GPUDescriptorHandleAllocator::heap() const +{ + return descriptor_heap_.Get(); +} + +void D3D12GPUDescriptorHandleAllocator::reset() +{ + index_ = 0u; +} + +} diff --git a/src/graphics/d3d12/d3d12_material.cpp b/src/graphics/d3d12/d3d12_material.cpp new file mode 100644 index 00000000..b82dac90 --- /dev/null +++ b/src/graphics/d3d12/d3d12_material.cpp @@ -0,0 +1,158 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/d3d12/d3d12_material.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +#include "core/error_handling.h" +#include "graphics/d3d12/d3d12_context.h" +#include "graphics/d3d12/hlsl_shader_compiler.h" +#include "graphics/lights/lighting_rig.h" +#include "graphics/shader_type.h" + +#pragma comment(lib, "d3dcompiler.lib") + +namespace +{ +/** + * Helper function to create a d3d12 shader. + * + * @param source + * Shader source. + * + * @param type + * Type of shader. + * + * @returns + * D3D12 handle to created shader. + */ +Microsoft::WRL::ComPtr create_shader(const std::string &source, iris::ShaderType type) +{ + const auto target = type == iris::ShaderType::VERTEX ? "vs_5_0" : "ps_5_0"; + + Microsoft::WRL::ComPtr shader = nullptr; + Microsoft::WRL::ComPtr error = nullptr; + if (::D3DCompile( + source.c_str(), + source.length(), + NULL, + NULL, + NULL, + "main", + target, + D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION, + 0u, + &shader, + &error) != S_OK) + { + const std::string error_message(static_cast(error->GetBufferPointer()), error->GetBufferSize()); + + throw iris::Exception("shader compile failed: " + error_message); + } + + return shader; +} + +} + +namespace iris +{ + +D3D12Material::D3D12Material( + const RenderGraph *render_graph, + const std::vector &input_descriptors, + PrimitiveType primitive_type, + LightType light_type, + bool render_to_swapchain) + : pso_() + , textures_() +{ + HLSLShaderCompiler compiler{render_graph, light_type}; + const auto vertex_source = compiler.vertex_shader(); + const auto fragment_source = compiler.fragment_shader(); + + const auto vertex_shader = create_shader(vertex_source, ShaderType::VERTEX); + const auto fragment_shader = create_shader(fragment_source, ShaderType::FRAGMENT); + + textures_ = compiler.textures(); + + auto *device = D3D12Context::device(); + auto *root_signature = D3D12Context::root_signature(); + + // setup various descriptors for pipeline state + + auto blend_state = CD3DX12_BLEND_DESC(D3D12_DEFAULT); + + // set blend mode based on light + // ambient is always rendered first (no blending) + // directional and point are always rendered after (blending) + blend_state.RenderTarget[0].BlendEnable = TRUE; + blend_state.RenderTarget[0].DestBlend = (light_type == LightType::AMBIENT) ? D3D12_BLEND_ZERO : D3D12_BLEND_ONE; + blend_state.RenderTarget[0].SrcBlend = D3D12_BLEND_ONE; + + auto depth_state = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT); + depth_state.DepthFunc = D3D12_COMPARISON_FUNC_LESS_EQUAL; + + D3D12_RASTERIZER_DESC rasterizer_description = {0}; + rasterizer_description.FillMode = D3D12_FILL_MODE_SOLID; + rasterizer_description.CullMode = D3D12_CULL_MODE_BACK; + rasterizer_description.FrontCounterClockwise = TRUE; + rasterizer_description.DepthClipEnable = TRUE; + rasterizer_description.ConservativeRaster = D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF; + + D3D12_GRAPHICS_PIPELINE_STATE_DESC descriptor = {}; + descriptor.InputLayout = {input_descriptors.data(), static_cast(input_descriptors.size())}; + descriptor.pRootSignature = root_signature; + descriptor.VS = CD3DX12_SHADER_BYTECODE(vertex_shader.Get()); + descriptor.PS = CD3DX12_SHADER_BYTECODE(fragment_shader.Get()); + descriptor.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); + descriptor.BlendState = blend_state; + descriptor.DepthStencilState = depth_state; + descriptor.DSVFormat = DXGI_FORMAT_D24_UNORM_S8_UINT; + descriptor.SampleMask = UINT_MAX; + descriptor.RasterizerState = rasterizer_description; + descriptor.NumRenderTargets = 1; + descriptor.RTVFormats[0] = !render_to_swapchain ? DXGI_FORMAT_R16G16B16A16_FLOAT : DXGI_FORMAT_R8G8B8A8_UNORM; + descriptor.SampleDesc.Count = 1; + + switch (primitive_type) + { + case PrimitiveType::TRIANGLES: descriptor.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; break; + case PrimitiveType::LINES: descriptor.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_LINE; break; + } + + // create pipeline state + expect(device->CreateGraphicsPipelineState(&descriptor, IID_PPV_ARGS(&pso_)) == S_OK, "could not create pso"); + + static int counter = 0; + std::wstringstream strm{}; + strm << L"pso_" << counter++; + const auto name = strm.str(); + pso_->SetName(name.c_str()); +} + +std::vector D3D12Material::textures() const +{ + return textures_; +} + +ID3D12PipelineState *D3D12Material::pso() const +{ + return pso_.Get(); +} + +} diff --git a/src/graphics/d3d12/d3d12_mesh.cpp b/src/graphics/d3d12/d3d12_mesh.cpp new file mode 100644 index 00000000..d201d5a7 --- /dev/null +++ b/src/graphics/d3d12/d3d12_mesh.cpp @@ -0,0 +1,103 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/d3d12/d3d12_mesh.h" + +#include +#include +#include + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +#include "core/exception.h" +#include "graphics/vertex_attributes.h" + +namespace +{ + +/** + * Helper function to map engine attribute types to D3D12 types. + * + * @param type + * Engine type + * + * @returns + * D3D12 type. + */ +DXGI_FORMAT to_directx_format(iris::VertexAttributeType type) +{ + auto format = DXGI_FORMAT_UNKNOWN; + + switch (type) + { + case iris::VertexAttributeType::FLOAT_3: format = DXGI_FORMAT_R32G32B32_FLOAT; break; + case iris::VertexAttributeType::FLOAT_4: format = DXGI_FORMAT_R32G32B32A32_FLOAT; break; + case iris::VertexAttributeType::UINT32_1: format = DXGI_FORMAT_R32_UINT; break; + case iris::VertexAttributeType::UINT32_4: format = DXGI_FORMAT_R32G32B32A32_UINT; break; + default: throw iris::Exception("unknown vertex attribute type"); + } + + return format; +} +} + +namespace iris +{ + +D3D12Mesh::D3D12Mesh( + const std::vector &vertices, + const std::vector &indices, + const VertexAttributes &attributes) + : vertex_buffer_(vertices) + , index_buffer_(indices) + , input_descriptors_() +{ + // build a D3D12 descriptors from supplied attributes + auto index = 0u; + for (const auto &[type, _1, _2, offset] : attributes) + { + // the engine doesn't care too much about vertex semantics, so we just + // call everything a TEXCOORD + input_descriptors_.push_back( + {"TEXCOORD", + index, + to_directx_format(type), + 0u, + static_cast(offset), + D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + 0u}); + + ++index; + } +} + +void D3D12Mesh::update_vertex_data(const std::vector &data) +{ + vertex_buffer_.write(data); +} + +void D3D12Mesh::update_index_data(const std::vector &data) +{ + index_buffer_.write(data); +} + +const D3D12Buffer &D3D12Mesh::vertex_buffer() const +{ + return vertex_buffer_; +} + +const D3D12Buffer &D3D12Mesh::index_buffer() const +{ + return index_buffer_; +} + +std::vector D3D12Mesh::input_descriptors() const +{ + return input_descriptors_; +} + +} diff --git a/src/graphics/d3d12/d3d12_mesh_manager.cpp b/src/graphics/d3d12/d3d12_mesh_manager.cpp new file mode 100644 index 00000000..839543aa --- /dev/null +++ b/src/graphics/d3d12/d3d12_mesh_manager.cpp @@ -0,0 +1,27 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/d3d12/d3d12_mesh_manager.h" + +#include +#include + +#include "graphics/d3d12/d3d12_mesh.h" +#include "graphics/mesh.h" +#include "graphics/mesh_manager.h" +#include "graphics/vertex_data.h" + +namespace iris +{ + +std::unique_ptr D3D12MeshManager::create_mesh( + const std::vector &vertices, + const std::vector &indices) const +{ + return std::make_unique(vertices, indices, DefaultVertexAttributes); +} + +} diff --git a/src/graphics/d3d12/d3d12_render_target.cpp b/src/graphics/d3d12/d3d12_render_target.cpp new file mode 100644 index 00000000..34b93489 --- /dev/null +++ b/src/graphics/d3d12/d3d12_render_target.cpp @@ -0,0 +1,46 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/d3d12/d3d12_render_target.h" + +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include "graphics/d3d12/d3d12_context.h" +#include "graphics/d3d12/d3d12_descriptor_handle.h" +#include "graphics/d3d12/d3d12_descriptor_manager.h" +#include "graphics/d3d12/d3d12_texture.h" +#include "graphics/texture.h" + +namespace iris +{ + +D3D12RenderTarget::D3D12RenderTarget( + std::unique_ptr colour_texture, + std::unique_ptr depth_texture) + : RenderTarget(std::move(colour_texture), std::move(depth_texture)) + , handle_() +{ + colour_texture_->set_flip(true); + depth_texture_->set_flip(true); + + auto *device = D3D12Context::device(); + + handle_ = D3D12DescriptorManager::cpu_allocator(D3D12_DESCRIPTOR_HEAP_TYPE_RTV).allocate_static(); + + device->CreateRenderTargetView( + static_cast(colour_texture_.get())->resource(), nullptr, handle_.cpu_handle()); +} + +D3D12DescriptorHandle D3D12RenderTarget::handle() const +{ + return handle_; +} + +} diff --git a/src/graphics/d3d12/d3d12_renderer.cpp b/src/graphics/d3d12/d3d12_renderer.cpp new file mode 100644 index 00000000..f2e98580 --- /dev/null +++ b/src/graphics/d3d12/d3d12_renderer.cpp @@ -0,0 +1,677 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/d3d12/d3d12_renderer.h" + +#include +#include +#include +#include +#include +#include +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +#include "core/error_handling.h" +#include "core/root.h" +#include "graphics/constant_buffer_writer.h" +#include "graphics/d3d12/d3d12_constant_buffer.h" +#include "graphics/d3d12/d3d12_context.h" +#include "graphics/d3d12/d3d12_descriptor_manager.h" +#include "graphics/d3d12/d3d12_mesh.h" +#include "graphics/d3d12/d3d12_render_target.h" +#include "graphics/d3d12/d3d12_texture.h" +#include "graphics/mesh_manager.h" +#include "graphics/render_entity.h" +#include "graphics/render_graph/post_processing_node.h" +#include "graphics/render_graph/texture_node.h" +#include "graphics/render_queue_builder.h" +#include "graphics/texture.h" +#include "graphics/texture_manager.h" +#include "graphics/window.h" +#include "graphics/window_manager.h" +#include "log/log.h" + +#pragma comment(lib, "dxgi.lib") +#pragma comment(lib, "d3d12.lib") +#pragma comment(lib, "dxguid.lib") + +namespace +{ + +// this matrix is used to translate projection matrices from engine NDC to +// metal NDC +static const iris::Matrix4 directx_translate{ + {1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f}}; + +/** + * Helper function to write vertex data into a constant buffer. + * + * @param constant_buffer + * D3D12ConstantBuffer object to write to. + * + * @param camera + * Camera for currently rendered scene. + * + * @param entity + * Entity being rendered. + * + * @param light_data + * Light for current render pass. + */ +void write_vertex_data_constant_buffer( + iris::D3D12ConstantBuffer &constant_buffer, + const iris::Camera *camera, + const iris::RenderEntity *entity, + const iris::Light *light) +{ + iris::ConstantBufferWriter writer(constant_buffer); + + writer.write(directx_translate * camera->projection()); + writer.write(camera->view()); + writer.write(camera->position()); + writer.write(0.0f); + writer.write(entity->transform()); + writer.write(entity->normal_transform()); + + const auto &bones = entity->skeleton().transforms(); + writer.write(bones); + writer.advance((100u - bones.size()) * sizeof(iris::Matrix4)); + + writer.write(light->colour_data()); + writer.write(light->world_space_data()); + writer.write(light->attenuation_data()); +} + +/** + * Helper function to write light specific data to a constant buffer. Note that + * this is for additional light data not written to the main vertex constant + * buffer. + * + * @param constant_buffer + * D3D12ConstantBuffer object to write to. + * + * @param light + * Light to get data from. + */ +void write_directional_light_data_constant_buffer(iris::D3D12ConstantBuffer &constant_buffer, const iris::Light *light) +{ + const auto *d3d12_light = static_cast(light); + + iris::ConstantBufferWriter writer(constant_buffer); + writer.write(directx_translate * d3d12_light->shadow_camera().projection()); + writer.write(d3d12_light->shadow_camera().view()); +} + +/** + * Helper function to copy a descriptor handle. + * + * @param dest + * Where to write the descriptor to. This is advanced by descriptor_size. + * + * @param source + * Source handle to copy from. + * + * @param descriptor_size + * Size of the descriptor handle. + */ +void copy_descriptor( + D3D12_CPU_DESCRIPTOR_HANDLE &dest, + const iris::D3D12DescriptorHandle &source, + std::size_t descriptor_size) +{ + auto *device = iris::D3D12Context::device(); + + device->CopyDescriptorsSimple(1u, dest, source.cpu_handle(), D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV); + + dest.ptr += descriptor_size; +} + +/** + * Build the table descriptor. + * + * @param table_descriptor + * The start of the table to write descriptors to. + * + * @param descriptor_size + * Size of the descriptor handle. + * + * @param vertex_constant_buffer + * D3D12DescriptorHandle for vertex data. + * + * @param light_constant_buffer + * D3D12DescriptorHandle for additional light data. + * + * @param shadow_map + * D3D12DescriptorHandle for shadow map texture. + * + * @param textures + * Collection of textures for the render pass. + */ +void build_table_descriptor( + D3D12_CPU_DESCRIPTOR_HANDLE &table_descriptor, + std::size_t descriptor_size, + const iris::D3D12DescriptorHandle &vertex_constant_buffer, + const iris::D3D12DescriptorHandle &light_constant_buffer, + const iris::D3D12DescriptorHandle &shadow_map, + const std::vector &textures) +{ + copy_descriptor(table_descriptor, vertex_constant_buffer, descriptor_size); + copy_descriptor(table_descriptor, light_constant_buffer, descriptor_size); + copy_descriptor(table_descriptor, shadow_map, descriptor_size); + + for (auto *texture : textures) + { + auto *d3d12_tex = static_cast(texture); + copy_descriptor(table_descriptor, d3d12_tex->handle(), descriptor_size); + } +} + +} + +namespace iris +{ + +D3D12Renderer::D3D12Renderer(HWND window, std::uint32_t width, std::uint32_t height, std::uint32_t initial_screen_scale) + : width_(width) + , height_(height) + , frames_() + , frame_index_(0u) + , command_queue_(nullptr) + , command_list_(nullptr) + , swap_chain_(nullptr) + , null_buffer_(nullptr) + , viewport_() + , scissor_rect_() + , render_targets_() + , materials_() + , uploaded_() + +{ + // we will use triple buffering + const auto num_frames = 3u; + + D3D12_COMMAND_QUEUE_DESC desc = {0}; + desc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; + desc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL; + desc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE; + desc.NodeMask = 0; + + auto *device = D3D12Context::device(); + auto *dxgi_factory = D3D12Context::dxgi_factory(); + + // create command queue + ensure(device->CreateCommandQueue(&desc, IID_PPV_ARGS(&command_queue_)) == S_OK, "could not create command queue"); + + // build swap chain description + DXGI_SWAP_CHAIN_DESC1 swap_chain_descriptor = {0}; + swap_chain_descriptor.Width = width_ * initial_screen_scale; + swap_chain_descriptor.Height = height_ * initial_screen_scale; + swap_chain_descriptor.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + swap_chain_descriptor.Stereo = FALSE; + swap_chain_descriptor.SampleDesc = {1, 0}; + swap_chain_descriptor.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + swap_chain_descriptor.BufferCount = num_frames; + swap_chain_descriptor.Scaling = DXGI_SCALING_STRETCH; + swap_chain_descriptor.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + swap_chain_descriptor.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED; + swap_chain_descriptor.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING; + + // create swap chain for window + Microsoft::WRL::ComPtr swap_chain_tmp = nullptr; + ensure( + dxgi_factory->CreateSwapChainForHwnd( + command_queue_.Get(), window, &swap_chain_descriptor, nullptr, nullptr, &swap_chain_tmp) == S_OK, + "could not create swap chain"); + + // cast to type we want to use + ensure(swap_chain_tmp.As(&swap_chain_) == S_OK, "could not cast swap chain"); + + // get initial frame index + frame_index_ = swap_chain_->GetCurrentBackBufferIndex(); + + // build our frame buffers + for (auto i = 0u; i < num_frames; ++i) + { + // get a back buffer + Microsoft::WRL::ComPtr frame = nullptr; + ensure(swap_chain_->GetBuffer(i, IID_PPV_ARGS(&frame)) == S_OK, "could not get back buffer"); + + static int counter = 0; + std::wstringstream strm{}; + strm << L"frame_" << counter++; + const auto name = strm.str(); + frame->SetName(name.c_str()); + + // create a static descriptor handle for the render target + auto rtv_handle = D3D12DescriptorManager::cpu_allocator(D3D12_DESCRIPTOR_HEAP_TYPE_RTV).allocate_static(); + + device->CreateRenderTargetView(frame.Get(), nullptr, rtv_handle.cpu_handle()); + + Microsoft::WRL::ComPtr command_allocator = nullptr; + + // create command allocator, we use one per frame but have a single + // command queue + ensure( + device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&command_allocator)) == S_OK, + "could not create command allocator"); + + // create a fence, used to signal when gpu has completed a frame + Microsoft::WRL::ComPtr fence = nullptr; + ensure(device->CreateFence(0u, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)) == S_OK, "could not create fence"); + + frames_.emplace_back( + frame, + rtv_handle, + std::make_unique( + DataBuffer{}, width_ * initial_screen_scale, height_ * initial_screen_scale, TextureUsage::DEPTH), + command_allocator, + fence, + ::CreateEvent(NULL, FALSE, TRUE, NULL)); + } + + ensure( + device->CreateCommandList( + 0u, + D3D12_COMMAND_LIST_TYPE_DIRECT, + frames_[frame_index_].command_allocator.Get(), + nullptr, + IID_PPV_ARGS(&command_list_)) == S_OK, + "could not create command list"); + + // create a single null buffer, used for padding buffer arrays + null_buffer_ = std::make_unique(); + + // close the list so we can start recording to it + command_list_->Close(); +} + +D3D12Renderer::~D3D12Renderer() +{ + // build a collection of all frame events + std::vector wait_handles{}; + + for (const auto &frame : frames_) + { + wait_handles.emplace_back(frame.fence_event); + } + + // we cannot destruct whilst a frame is being rendered, so we wait for all + // frames to signal they are done + ::WaitForMultipleObjects(static_cast(wait_handles.size()), wait_handles.data(), TRUE, INFINITE); +} + +void D3D12Renderer::set_render_passes(const std::vector &render_passes) +{ + render_passes_ = render_passes; + + // add a post processing pass + + // find the pass which renders to the screen + auto final_pass = std::find_if( + std::begin(render_passes_), + std::end(render_passes_), + [](const RenderPass &pass) { return pass.render_target == nullptr; }); + + ensure(final_pass != std::cend(render_passes_), "no final pass"); + + // deferred creating of render target to ensure this class is full + // constructed + if (post_processing_target_ == nullptr) + { + post_processing_target_ = create_render_target(width_, height_); + post_processing_camera_ = std::make_unique(CameraType::ORTHOGRAPHIC, width_, height_); + } + + post_processing_scene_ = std::make_unique(); + + // create a full screen quad which renders the final stage with the post + // processing node + auto *rg = post_processing_scene_->create_render_graph(); + rg->set_render_node(rg->create(post_processing_target_->colour_texture())); + post_processing_scene_->create_entity( + rg, + Root::mesh_manager().sprite({}), + Transform({}, {}, {static_cast(width_), static_cast(height_), 1.0})); + + // wire up this pass + final_pass->render_target = post_processing_target_; + render_passes_.emplace_back(post_processing_scene_.get(), post_processing_camera_.get(), nullptr); + + // build the render queue from the provided passes + + RenderQueueBuilder queue_builder( + [this](RenderGraph *render_graph, RenderEntity *render_entity, const RenderTarget *target, LightType light_type) + { + if (materials_.count(render_graph) == 0u || materials_[render_graph].count(light_type) == 0u) + { + materials_[render_graph][light_type] = std::make_unique( + render_graph, + static_cast(render_entity->mesh())->input_descriptors(), + render_entity->primitive_type(), + light_type, + target == nullptr); + } + + return materials_[render_graph][light_type].get(); + }, + [this](std::uint32_t width, std::uint32_t height) { return create_render_target(width, height); }); + render_queue_ = queue_builder.build(render_passes_); + + // clear all constant data buffers + for (auto &frame : frames_) + { + frame.constant_data_buffers.clear(); + } + + // create a constant data buffer for each draw command + for (const auto &command : render_queue_) + { + if (command.type() == RenderCommandType::DRAW) + { + const auto *command_ptr = std::addressof(command); + + for (auto &frame : frames_) + { + frame.constant_data_buffers.emplace(command_ptr, D3D12ConstantBufferPool{}); + } + } + } +} + +RenderTarget *D3D12Renderer::create_render_target(std::uint32_t width, std::uint32_t height) +{ + const auto scale = Root::window_manager().current_window()->screen_scale(); + + auto colour_texture = + std::make_unique(DataBuffer{}, width * scale, height * scale, TextureUsage::RENDER_TARGET); + auto depth_texture = + std::make_unique(DataBuffer{}, width * scale, height * scale, TextureUsage::DEPTH); + + // add these to uploaded so the next render pass doesn't try to upload them + uploaded_.emplace(colour_texture.get()); + uploaded_.emplace(depth_texture.get()); + + render_targets_.emplace_back( + std::make_unique(std::move(colour_texture), std::move(depth_texture))); + + auto *rt = render_targets_.back().get(); + + return rt; +} + +void D3D12Renderer::pre_render() +{ + const auto &frame = frames_[frame_index_]; + + // if the gpu is still using this frame then wait + ::WaitForSingleObject(frame.fence_event, INFINITE); + + // reset state to non-waiting + frame.fence->Signal(0u); + ::ResetEvent(frame.fence_event); + + // reset command allocator and list for new frame + frame.command_allocator->Reset(); + command_list_->Reset(frame.command_allocator.Get(), nullptr); + + // reset descriptor allocations for new frame + D3D12DescriptorManager::gpu_allocator(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV).reset(); + + D3D12DescriptorManager::cpu_allocator(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV).reset_dynamic(); +} + +void D3D12Renderer::execute_upload_texture(RenderCommand &command) +{ + const auto *material = static_cast(command.material()); + + // encode commands to copy all textures to their target heaps + for (auto *texture : material->textures()) + { + const auto *d3d12_tex = static_cast(texture); + + // only upload once + if (uploaded_.count(d3d12_tex) == 0u) + { + uploaded_.emplace(d3d12_tex); + + D3D12_TEXTURE_COPY_LOCATION destination = {}; + destination.pResource = d3d12_tex->resource(); + destination.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + destination.SubresourceIndex = 0u; + + D3D12_TEXTURE_COPY_LOCATION source = {}; + source.pResource = d3d12_tex->upload(); + source.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + source.PlacedFootprint = d3d12_tex->footprint(); + + command_list_->CopyTextureRegion(&destination, 0u, 0u, 0u, &source, NULL); + + const auto barrier = ::CD3DX12_RESOURCE_BARRIER::Transition( + d3d12_tex->resource(), D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + + command_list_->ResourceBarrier(1u, &barrier); + } + } +} + +void D3D12Renderer::execute_pass_start(RenderCommand &command) +{ + ID3D12DescriptorHeap *heaps[] = { + D3D12DescriptorManager::gpu_allocator(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV).heap()}; + command_list_->SetDescriptorHeaps(_countof(heaps), heaps); + + const Colour clear_colour{0.4f, 0.6f, 0.9f, 1.0f}; + + D3D12_CPU_DESCRIPTOR_HANDLE rt_handle; + D3D12_CPU_DESCRIPTOR_HANDLE depth_handle; + + auto *target = static_cast(command.render_pass()->render_target); + + const auto scale = Root::window_manager().current_window()->screen_scale(); + auto width = width_ * scale; + auto height = height_ * scale; + + const auto &frame = frames_[frame_index_]; + + if (target == nullptr) + { + rt_handle = frame.render_target.cpu_handle(); + depth_handle = frame.depth_buffer->depth_handle().cpu_handle(); + + // if the current frame is the default render target i.e. not one + // manually created we need to transition it from PRESENT and make the + // depth buffer writable + const D3D12_RESOURCE_BARRIER barriers[] = { + ::CD3DX12_RESOURCE_BARRIER::Transition( + frame.buffer.Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET), + ::CD3DX12_RESOURCE_BARRIER::Transition( + frame.depth_buffer->resource(), + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, + D3D12_RESOURCE_STATE_DEPTH_WRITE)}; + + command_list_->ResourceBarrier(2u, barriers); + } + else + { + width = target->width(); + height = target->height(); + + rt_handle = static_cast(target)->handle().cpu_handle(); + depth_handle = static_cast(target->depth_texture())->depth_handle().cpu_handle(); + + // if we are rendering to a custom render target we just need to make + // its depth buffer writable + + const auto barrier = ::CD3DX12_RESOURCE_BARRIER::Transition( + static_cast(target->depth_texture())->resource(), + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, + D3D12_RESOURCE_STATE_DEPTH_WRITE); + + command_list_->ResourceBarrier(1u, &barrier); + } + + // setup and clear render target + command_list_->OMSetRenderTargets(1, &rt_handle, FALSE, &depth_handle); + command_list_->ClearRenderTargetView(rt_handle, reinterpret_cast(&clear_colour), 0, nullptr); + command_list_->ClearDepthStencilView(depth_handle, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0u, 0u, nullptr); + + command_list_->SetGraphicsRootSignature(D3D12Context::root_signature()); + + // update viewport incase it's changed for current render target + viewport_ = CD3DX12_VIEWPORT{0.0f, 0.0f, static_cast(width), static_cast(height)}; + scissor_rect_ = CD3DX12_RECT{0u, 0u, static_cast(width), static_cast(height)}; + + command_list_->RSSetViewports(1u, &viewport_); + command_list_->RSSetScissorRects(1u, &scissor_rect_); +} + +void D3D12Renderer::execute_pass_end(RenderCommand &command) +{ + const auto *target = static_cast(command.render_pass()->render_target); + + if (target != nullptr) + { + // if we are rendering to a custom render target then we need to make + // the depth buffer accessible to the shader + const auto barrier = ::CD3DX12_RESOURCE_BARRIER::Transition( + static_cast(target->depth_texture())->resource(), + D3D12_RESOURCE_STATE_DEPTH_WRITE, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE); + + command_list_->ResourceBarrier(1u, &barrier); + } +} + +void D3D12Renderer::execute_draw(RenderCommand &command) +{ + const auto *entity = command.render_entity(); + const auto *material = static_cast(command.material()); + const auto *mesh = static_cast(entity->mesh()); + const auto *camera = command.render_pass()->camera; + const auto *light = command.light(); + const auto *shadow_map = command.shadow_map(); + + command_list_->SetPipelineState(material->pso()); + + auto &frame = frames_[frame_index_]; + + // create a table descriptor which is a continuous block of all descriptors + // needed by the shader + // the number of descriptors is set by the root signature but this is how + // we organise them: + // + // .-+--------------------+ + // | | vertex data | + // constant data | +--------------------+ + // | | light data | + // '-+--------------------+-. + // | shadow map texture | | + // +--------------------+ | + // | texture 1 | | + // +--------------------+ | + // | texture 2 | | shader resources + // +--------------------+ | + // | texture 3 | | + // +--------------------+ | + // | texture 4 | | + // +--------------------+-' + const auto table_descriptors = D3D12DescriptorManager::gpu_allocator(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV) + .allocate(D3D12Context::num_descriptors()); + const auto descriptor_size = + D3D12DescriptorManager::gpu_allocator(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV).descriptor_size(); + + auto table_descriptor_start = table_descriptors.cpu_handle(); + + // create and write our constant data buffers + auto &vertex_buffer = frame.constant_data_buffers.at(std::addressof(command)).next(); + write_vertex_data_constant_buffer(vertex_buffer, camera, entity, light); + + auto &light_buffer = frame.constant_data_buffers.at(std::addressof(command)).next(); + write_directional_light_data_constant_buffer(light_buffer, light); + + // create handles to light and shadow map data, these may be a null handle + // depending on the material + + const auto light_data_handle = (light->type() == LightType::DIRECTIONAL) ? light_buffer.descriptor_handle() + : null_buffer_->descriptor_handle(); + + auto shadow_map_handle = (shadow_map == nullptr) + ? static_cast(Root::texture_manager().blank())->handle() + : static_cast(command.shadow_map()->depth_texture())->handle(); + + // build the table descriptor from all our handles + build_table_descriptor( + table_descriptor_start, + descriptor_size, + vertex_buffer.descriptor_handle(), + light_buffer.descriptor_handle(), + shadow_map_handle, + material->textures()); + + // set the table descriptor for the vertex and pixel shader + command_list_->SetGraphicsRootDescriptorTable(0u, table_descriptors.gpu_handle()); + command_list_->SetGraphicsRootDescriptorTable(1u, table_descriptors.gpu_handle()); + + const auto vertex_view = mesh->vertex_buffer().vertex_view(); + const auto index_view = mesh->index_buffer().index_view(); + const auto num_indices = static_cast(mesh->index_buffer().element_count()); + + switch (entity->primitive_type()) + { + case PrimitiveType::TRIANGLES: + command_list_->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + break; + case PrimitiveType::LINES: command_list_->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_LINELIST); break; + } + + command_list_->IASetVertexBuffers(0u, 1u, &vertex_view); + command_list_->IASetIndexBuffer(&index_view); + command_list_->DrawIndexedInstanced(num_indices, 1u, 0u, 0u, 0u); +} + +void D3D12Renderer::execute_present(RenderCommand &) +{ + const auto &frame = frames_[frame_index_]; + + // transition the frame render target to present and depth buffer to shader + // visible + const D3D12_RESOURCE_BARRIER barriers[] = { + ::CD3DX12_RESOURCE_BARRIER::Transition( + frame.buffer.Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT), + ::CD3DX12_RESOURCE_BARRIER::Transition( + frame.depth_buffer->resource(), + D3D12_RESOURCE_STATE_DEPTH_WRITE, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE)}; + + command_list_->ResourceBarrier(2u, barriers); + + command_list_->Close(); + + // execute command list + ID3D12CommandList *const command_lists[] = {command_list_.Get()}; + command_queue_->ExecuteCommandLists(1u, command_lists); + + // present frame to window + expect(swap_chain_->Present(0u, 0u) == S_OK, "could not present"); + + // enqueue signal so future render passes know when the frame is safe to use + expect(command_queue_->Signal(frame.fence.Get(), 1u) == S_OK, "could not signal"); + frame.fence->SetEventOnCompletion(1u, frame.fence_event); + + frame_index_ = swap_chain_->GetCurrentBackBufferIndex(); +} + +} diff --git a/src/graphics/d3d12/d3d12_texture.cpp b/src/graphics/d3d12/d3d12_texture.cpp new file mode 100644 index 00000000..1a3080aa --- /dev/null +++ b/src/graphics/d3d12/d3d12_texture.cpp @@ -0,0 +1,418 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/d3d12/d3d12_texture.h" + +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include + +#include "directx/d3d12.h" +#include "directx/d3dx12.h" + +#include "core/colour.h" +#include "core/error_handling.h" +#include "graphics/d3d12/d3d12_context.h" +#include "graphics/d3d12/d3d12_descriptor_handle.h" +#include "graphics/d3d12/d3d12_descriptor_manager.h" +#include "graphics/texture_usage.h" + +namespace +{ + +/** + * Helper function to set the name of the ID3D12Resource. + * + * The prefix will have a unique integer appended, hence why this is templated, + * so each enum type gets its own range of numbers. + * + * @param prefix + * Prefix for name. + * + * @param resource + * The resource to set the name of. + */ +template +void set_name(const std::wstring &prefix, ID3D12Resource *resource) +{ + static int counter = 0; + + std::wstringstream strm{}; + strm << prefix << L"_" << counter++; + const auto name = strm.str(); + + resource->SetName(name.c_str()); +} + +/** + * Helper function to create a d3d12 resource description suitable for the + * texture usage. + * + * @param width + * Width of texture. + * + * @param height + * Height of texture. + * + * @param resource + * Handle to store created resource in. + * + * @returns + * D3D12_RESOURCE_DESC for texture. + */ +D3D12_RESOURCE_DESC image_texture_descriptor( + std::uint32_t width, + std::uint32_t height, + Microsoft::WRL::ComPtr &resource) +{ + auto *device = iris::D3D12Context::device(); + const auto default_heap = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT); + + D3D12_RESOURCE_DESC texture_description{}; + texture_description.Format = DXGI_FORMAT_R8G8B8A8_UNORM_SRGB; + texture_description.Width = width; + texture_description.Height = height; + texture_description.Flags = D3D12_RESOURCE_FLAG_NONE; + texture_description.DepthOrArraySize = 1; + texture_description.MipLevels = 1; + texture_description.SampleDesc.Count = 1; + texture_description.SampleDesc.Quality = 0; + texture_description.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + texture_description.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + texture_description.Alignment = 0; + + // create a resource where image data will be coped to + iris::expect( + device->CreateCommittedResource( + &default_heap, + D3D12_HEAP_FLAG_NONE, + &texture_description, + D3D12_RESOURCE_STATE_COPY_DEST, + nullptr, + IID_PPV_ARGS(&resource)) == S_OK, + "could not create resource"); + + set_name(L"tex", resource.Get()); + + return texture_description; +} + +/** + * Helper function to create a d3d12 resource description suitable for the + * data usage. + * + * @param width + * Width of texture. + * + * @param height + * Height of texture. + * + * @param resource + * Handle to store created resource in. + * + * @returns + * D3D12_RESOURCE_DESC for texture. + */ +D3D12_RESOURCE_DESC data_texture_descriptor( + std::uint32_t width, + std::uint32_t height, + Microsoft::WRL::ComPtr &resource) +{ + auto *device = iris::D3D12Context::device(); + const auto default_heap = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT); + + D3D12_RESOURCE_DESC texture_description{}; + texture_description.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + texture_description.Width = width; + texture_description.Height = height; + texture_description.Flags = D3D12_RESOURCE_FLAG_NONE; + texture_description.DepthOrArraySize = 1; + texture_description.MipLevels = 1; + texture_description.SampleDesc.Count = 1; + texture_description.SampleDesc.Quality = 0; + texture_description.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + texture_description.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + texture_description.Alignment = 0; + + // create a resource where image data will be coped to + iris::expect( + device->CreateCommittedResource( + &default_heap, + D3D12_HEAP_FLAG_NONE, + &texture_description, + D3D12_RESOURCE_STATE_COPY_DEST, + nullptr, + IID_PPV_ARGS(&resource)) == S_OK, + "could not create resource"); + + set_name(L"tex", resource.Get()); + + return texture_description; +} + +/** + * Helper function to create a d3d12 resource description suitable for the + * render target usage. + * + * @param width + * Width of texture. + * + * @param height + * Height of texture. + * + * @param resource + * Handle to store created resource in. + * + * @returns + * D3D12_RESOURCE_DESC for texture. + */ +D3D12_RESOURCE_DESC +render_target_texture_descriptor( + std::uint32_t width, + std::uint32_t height, + Microsoft::WRL::ComPtr &resource) +{ + auto *device = iris::D3D12Context::device(); + const auto default_heap = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT); + + D3D12_RESOURCE_DESC texture_description{}; + texture_description.Format = DXGI_FORMAT_R16G16B16A16_FLOAT; + texture_description.Width = width; + texture_description.Height = height; + texture_description.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; + texture_description.DepthOrArraySize = 1; + texture_description.MipLevels = 1; + texture_description.SampleDesc.Count = 1; + texture_description.SampleDesc.Quality = 0; + texture_description.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + texture_description.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + texture_description.Alignment = 0; + + const iris::Colour clear_colour{0.4f, 0.6f, 0.9f, 1.0f}; + D3D12_CLEAR_VALUE clear_value = {0}; + clear_value.Format = texture_description.Format; + std::memcpy(&clear_value.Color, &clear_colour, sizeof(clear_colour)); + + // create a resource where image data will be coped to + iris::expect( + device->CreateCommittedResource( + &default_heap, + D3D12_HEAP_FLAG_NONE, + &texture_description, + D3D12_RESOURCE_STATE_RENDER_TARGET, + &clear_value, + IID_PPV_ARGS(&resource)) == S_OK, + "could not create resource"); + + set_name(L"rt", resource.Get()); + + return texture_description; +} + +/** + * Helper function to create a d3d12 resource description suitable for the + * depth usage. + * + * @param width + * Width of texture. + * + * @param height + * Height of texture. + * + * @param resource + * Handle to store created resource in. + * + * @returns + * D3D12_RESOURCE_DESC for texture. + */ +D3D12_RESOURCE_DESC depth_texture_descriptor( + std::uint32_t width, + std::uint32_t height, + Microsoft::WRL::ComPtr &resource) +{ + auto *device = iris::D3D12Context::device(); + const auto default_heap = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT); + + D3D12_RESOURCE_DESC texture_description{}; + texture_description.Format = DXGI_FORMAT_R24G8_TYPELESS; + texture_description.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + texture_description.Alignment = 0; + texture_description.Width = width; + texture_description.Height = height; + texture_description.DepthOrArraySize = 1; + texture_description.MipLevels = 1; + texture_description.SampleDesc.Count = 1; + texture_description.SampleDesc.Quality = 0; + texture_description.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + texture_description.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL; + + D3D12_CLEAR_VALUE clear_value = {}; + clear_value.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; + clear_value.DepthStencil.Depth = 1.0f; + clear_value.DepthStencil.Stencil = 0u; + + iris::expect( + device->CreateCommittedResource( + &default_heap, + D3D12_HEAP_FLAG_NONE, + &texture_description, + D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE, + &clear_value, + IID_PPV_ARGS(&resource)) == S_OK, + "could not create resource"); + + set_name(L"depth", resource.Get()); + + return texture_description; +} + +} + +namespace iris +{ + +D3D12Texture::D3D12Texture(const DataBuffer &data, std::uint32_t width, std::uint32_t height, TextureUsage usage) + : Texture(data, width, height, usage) + , resource_() + , upload_() + , resource_view_() + , depth_resource_view_() + , footprint_() + , type_() +{ + auto *device = D3D12Context::device(); + const auto default_heap = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT); + + const auto &texture_description = [this, usage]() + { + switch (usage) + { + case TextureUsage::IMAGE: return image_texture_descriptor(width_, height_, resource_); break; + case TextureUsage::DATA: return data_texture_descriptor(width_, height_, resource_); break; + case TextureUsage::RENDER_TARGET: + return render_target_texture_descriptor(width_, height_, resource_); + break; + case TextureUsage::DEPTH: return depth_texture_descriptor(width_, height_, resource_); break; + default: throw Exception("unknown texture usage"); + } + }(); + + // finish off setting up the resource, note that DEPTH has some some special + // case handling + + if (usage != TextureUsage::DEPTH) + { + + const UINT64 capacity = GetRequiredIntermediateSize(resource_.Get(), 0, 1); + + const auto upload_heap = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD); + const auto heap_description = CD3DX12_RESOURCE_DESC::Buffer(capacity); + + // create resource for initial upload of texture data + expect( + device->CreateCommittedResource( + &upload_heap, + D3D12_HEAP_FLAG_NONE, + &heap_description, + D3D12_RESOURCE_STATE_GENERIC_READ, + nullptr, + IID_PPV_ARGS(&upload_)) == S_OK, + "could not create resource"); + + type_ = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; + + resource_view_ = D3D12DescriptorManager::cpu_allocator(type_).allocate_static(); + + device->CreateShaderResourceView(resource_.Get(), NULL, resource_view_.cpu_handle()); + + // map the upload buffer so we can write to it + void *mapped_buffer = nullptr; + expect( + upload_->Map(0u, NULL, reinterpret_cast(&mapped_buffer)) == S_OK, "failed to map constant buffer"); + + if (!data.empty()) + { + UINT heights[] = {height_}; + UINT64 row_size[] = {width_ * 4u}; + + // create footprint for image data layout + std::uint64_t memory_size = 0u; + device->GetCopyableFootprints(&texture_description, 0, 1, 0, &footprint_, heights, row_size, &memory_size); + + auto *dst_cursor = reinterpret_cast(mapped_buffer); + auto *src_cursor = data_.data(); + + // copy texture data with respect to footprint + for (auto i = 0u; i < height_; ++i) + { + std::memcpy(dst_cursor, src_cursor, row_size[0]); + dst_cursor += footprint_.Footprint.RowPitch; + src_cursor += row_size[0]; + } + } + } + else + { + type_ = D3D12_DESCRIPTOR_HEAP_TYPE_DSV; + + depth_resource_view_ = D3D12DescriptorManager::cpu_allocator(type_).allocate_static(); + + D3D12_DEPTH_STENCIL_VIEW_DESC depth_view_description = {0}; + depth_view_description.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; + depth_view_description.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D; + depth_view_description.Flags = D3D12_DSV_FLAG_NONE; + depth_view_description.Texture2D.MipSlice = 0; + + // create the depth/stencil view into the texture + device->CreateDepthStencilView(resource_.Get(), &depth_view_description, depth_resource_view_.cpu_handle()); + + resource_view_ = + D3D12DescriptorManager::cpu_allocator(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV).allocate_static(); + + D3D12_SHADER_RESOURCE_VIEW_DESC shader_view_description = {0}; + shader_view_description.Format = DXGI_FORMAT_R24_UNORM_X8_TYPELESS; + shader_view_description.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + shader_view_description.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; + shader_view_description.Texture2D.MipLevels = 1; + shader_view_description.Texture2D.MostDetailedMip = 0; + + device->CreateShaderResourceView(resource_.Get(), &shader_view_description, resource_view_.cpu_handle()); + } +} + +ID3D12Resource *D3D12Texture::resource() const +{ + return resource_.Get(); +} + +ID3D12Resource *D3D12Texture::upload() const +{ + return upload_.Get(); +} + +D3D12_PLACED_SUBRESOURCE_FOOTPRINT D3D12Texture::footprint() const +{ + return footprint_; +} + +D3D12DescriptorHandle D3D12Texture::handle() const +{ + return resource_view_; +} + +D3D12DescriptorHandle D3D12Texture::depth_handle() const +{ + return depth_resource_view_; +} + +D3D12_DESCRIPTOR_HEAP_TYPE D3D12Texture::type() const +{ + return type_; +} + +} diff --git a/src/graphics/d3d12/d3d12_texture_manager.cpp b/src/graphics/d3d12/d3d12_texture_manager.cpp new file mode 100644 index 00000000..80664dd6 --- /dev/null +++ b/src/graphics/d3d12/d3d12_texture_manager.cpp @@ -0,0 +1,30 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/d3d12/d3d12_texture_manager.h" + +#include +#include + +#include "core/data_buffer.h" +#include "graphics/d3d12/d3d12_texture.h" +#include "graphics/texture.h" +#include "graphics/texture_manager.h" +#include "graphics/texture_usage.h" + +namespace iris +{ +std::unique_ptr D3D12TextureManager::do_create( + const DataBuffer &data, + std::uint32_t width, + std::uint32_t height, + TextureUsage usage) + +{ + return std::make_unique(data, width, height, usage); +} + +} diff --git a/src/graphics/d3d12/hlsl_shader_compiler.cpp b/src/graphics/d3d12/hlsl_shader_compiler.cpp new file mode 100644 index 00000000..cab6736b --- /dev/null +++ b/src/graphics/d3d12/hlsl_shader_compiler.cpp @@ -0,0 +1,664 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/d3d12/hlsl_shader_compiler.h" + +#include +#include +#include + +#include "core/colour.h" +#include "core/exception.h" +#include "core/vector3.h" +#include "graphics/lights/lighting_rig.h" +#include "graphics/render_graph/arithmetic_node.h" +#include "graphics/render_graph/blur_node.h" +#include "graphics/render_graph/colour_node.h" +#include "graphics/render_graph/combine_node.h" +#include "graphics/render_graph/component_node.h" +#include "graphics/render_graph/composite_node.h" +#include "graphics/render_graph/conditional_node.h" +#include "graphics/render_graph/invert_node.h" +#include "graphics/render_graph/post_processing_node.h" +#include "graphics/render_graph/render_node.h" +#include "graphics/render_graph/sin_node.h" +#include "graphics/render_graph/texture_node.h" +#include "graphics/render_graph/value_node.h" +#include "graphics/render_graph/vertex_position_node.h" +#include "graphics/texture.h" + +namespace +{ + +static constexpr auto uniforms = R"( +cbuffer DefaultUniforms : register(b0) +{ + matrix projection; + matrix view; + float4 camera; + matrix model; + matrix normal_matrix; + matrix bones[100]; + float4 light_colour; + float4 light_position; + float4 light_attenuation; +}; + +cbuffer DirectionalLight : register(b1) +{ + matrix light_projection; + matrix light_view; +};)"; + +static constexpr auto ps_input = R"( +struct PSInput +{ + float4 position : SV_POSITION; + float4 frag_position : POSITION0; + float4 tangent_view_pos : POSITION1; + float4 tangent_frag_pos : POSITION2; + float4 tangent_light_pos : POSITION3; + float4 frag_pos_light_space : POSITION4; + float4 normal : NORMAL; + float4 colour : COLOR; + float2 tex_coord : TEXCOORD; +};)"; + +static constexpr auto invert_function = R"( +float4 invert(float4 colour) +{ + return float4(1.0 - colour.r, 1.0 - colour.g, 1.0 - colour.b, colour.a); +})"; + +static constexpr auto composite_function = R"( +float4 composite(float4 colour1, float4 colour2, float4 depth1, float4 depth2, float2 tex_coord) +{ + float4 colour = colour2; + + if(depth1.r < depth2.r) + { + colour = colour1; + } + + return colour; +})"; + +static constexpr auto blur_function = R"( +float4 blur(Texture2D tex, float2 tex_coords) +{ + const float offset = 1.0 / 500.0; + float2 offsets[9] = { + float2(-offset, offset), // top-left + float2( 0.0f, offset), // top-center + float2( offset, offset), // top-right + float2(-offset, 0.0f), // center-left + float2( 0.0f, 0.0f), // center-center + float2( offset, 0.0f), // center-right + float2(-offset, -offset), // bottom-left + float2( 0.0f, -offset), // bottom-center + float2( offset, -offset) // bottom-right + }; + + float k[9] = { + 1.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0, + 2.0 / 16.0, 4.0 / 16.0, 2.0 / 16.0, + 1.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0 + }; + + + float3 sampleTex[9]; + for(int i = 0; i < 9; i++) + { + sampleTex[i] = tex.Sample(g_sampler, tex_coords + offsets[i]); + } + + float3 col = float3(0.0, 0.0, 0.0); + for(int i = 0; i < 9; i++) + { + col += sampleTex[i] * k[i]; + } + return float4(col, 1.0); +})"; + +static constexpr auto shadow_function = R"( +float calculate_shadow(float3 n, float4 frag_pos_light_space, float3 light_dir, Texture2D tex) +{ + float shadow = 0.0; + + float3 proj_coord = frag_pos_light_space.xyz / frag_pos_light_space.w; + + float2 proj_uv = float2(proj_coord.x, -proj_coord.y); + proj_uv = proj_uv * 0.5 + 0.5; + + float closest_depth = tex.Sample(g_sampler, proj_uv).r; + float current_depth = proj_coord.z; + float bias = 0.001;// max(0.05 * (1.0 - dot(n, light_dir)), 0.005); + + shadow = current_depth - bias > closest_depth ? 1.0 : 0.0; + if(proj_coord.z > 1.0) + { + shadow = 0.0; + } + + return shadow; +})"; + +/** + * Helper function to create a unique id for a texture. Always returns the + * same name for the same texture. + * + * @param texture + * Texture to generate id for. + * + * @param textures + * Collection of textures, texture will be inserted if it does not exist. + * + * @returns + * Unique id for the texture. + */ +std::size_t texture_id(iris::Texture *texture, std::vector &textures) +{ + std::size_t id = 0u; + + const auto find = std::find(std::cbegin(textures), std::cend(textures), texture); + + if (find != std::cend(textures)) + { + id = std::distance(std::cbegin(textures), find); + } + else + { + id = textures.size(); + textures.emplace_back(texture); + } + + return id + 1u; +} + +/** + * Visit a node if it exists or write out a default value to a stream. + * + * This helper function is a little bit brittle as it assumes the visitor will + * write to the same stream as the one supplied. + * + * @param strm + * Stream to write to. + * + * @param node + * Node to visit, if nullptr then default value will be written to stream. + * + * @param visitor + * Visitor to visit node if not null. + * + * @param default_value + * Value to write to stream if node not null. + * + * @param add_semi_colon + * If true write semi colon after visit/value, else do nothing. + */ +void visit_or_default( + std::stringstream &strm, + const iris::Node *node, + iris::HLSLShaderCompiler *visitor, + const std::string &default_value, + bool add_semi_colon = true) +{ + if (node == nullptr) + { + strm << default_value; + } + else + { + node->accept(*visitor); + } + + if (add_semi_colon) + { + strm << ";\n"; + } +} + +/** + * Write shader code for generating fragment colour. + * + * @param strm + * Stream to write to. + * + * @param colour + * Node for colour (maybe nullptr). + * + * @param visitor + * Visitor for node. + */ +void build_fragment_colour(std::stringstream &strm, const iris::Node *colour, iris::HLSLShaderCompiler *visitor) +{ + strm << "float4 fragment_colour = "; + visit_or_default(strm, colour, visitor, "input.colour"); +} + +/** + * Write shader code for generating fragment normal. + * + * @param strm + * Stream to write to. + * + * @param normal + * Node for normal (maybe nullptr). + * + * @param visitor + * Visitor for node. + */ +void build_normal(std::stringstream &strm, const iris::Node *normal, iris::HLSLShaderCompiler *visitor) +{ + strm << "float3 n = "; + if (normal == nullptr) + { + strm << "normalize(input.normal.xyz);\n"; + } + else + { + strm << "float3("; + normal->accept(*visitor); + strm << ".xyz);\n"; + strm << "n = normalize(n * 2.0 - 1.0);\n"; + } +} + +} + +namespace iris +{ +HLSLShaderCompiler::HLSLShaderCompiler(const RenderGraph *render_graph, LightType light_type) + : vertex_stream_() + , fragment_stream_() + , current_stream_(nullptr) + , vertex_functions_() + , fragment_functions_() + , current_functions_(nullptr) + , textures_() + , light_type_(light_type) +{ + render_graph->render_node()->accept(*this); +} + +void HLSLShaderCompiler::visit(const RenderNode &node) +{ + current_stream_ = &vertex_stream_; + current_functions_ = &vertex_functions_; + + // build vertex shader + + *current_stream_ << R"( +PSInput main( + float4 position : TEXCOORD0, + float4 normal : TEXCOORD1, + float4 colour : TEXCOORD2, + float4 tex_coord : TEXCOORD3, + float4 tangent : TEXCOORD4, + float4 bitangent : TEXCOORD5, + uint4 bone_ids : TEXCOORD6, + float4 bone_weights : TEXCOORD7) +{ + matrix bone_transform = mul(bones[bone_ids[0]], bone_weights[0]); + bone_transform += mul(bones[bone_ids[1]], bone_weights[1]); + bone_transform += mul(bones[bone_ids[2]], bone_weights[2]); + bone_transform += mul(bones[bone_ids[3]], bone_weights[3]); + + float3 T = normalize(mul(mul(bone_transform, tangent), normal_matrix).xyz); + float3 B = normalize(mul(mul(bone_transform, bitangent), normal_matrix).xyz); + float3 N = normalize(mul(mul(bone_transform, normal), normal_matrix).xyz); + + float3x3 tbn = transpose(float3x3(T, B, N)); + + PSInput result; + + result.frag_position = mul(position, bone_transform); + result.frag_position = mul(result.frag_position, model); + + result.tangent_light_pos = float4(mul(light_position.xyz, tbn), 0.0); + result.tangent_view_pos = float4(mul(camera.xyz, tbn), 0.0); + result.tangent_frag_pos = float4(mul(result.frag_position, tbn), 0.0); +)"; + if (light_type_ == LightType::DIRECTIONAL) + { + *current_stream_ << R"( + result.frag_pos_light_space = mul(result.frag_position, light_view); + result.frag_pos_light_space = mul(result.frag_pos_light_space, light_projection); +)"; + } + *current_stream_ << R"( + result.position = mul(result.frag_position, view); + result.position = mul(result.position, projection); + result.normal = mul(normal, bone_transform); + result.normal = mul(result.normal, normal_matrix); + result.colour = colour; + result.tex_coord = tex_coord; + + return result; +})"; + + current_stream_ = &fragment_stream_; + current_functions_ = &fragment_functions_; + + current_functions_->emplace(shadow_function); + + // build fragment shader + + *current_stream_ << R"( +float4 main(PSInput input) : SV_TARGET +{)"; + + build_fragment_colour(*current_stream_, node.colour_input(), this); + build_normal(*current_stream_, node.normal_input(), this); + + // depending on the light type depends on how we interpret the light + // constant data and how we calculate lighting + switch (light_type_) + { + case LightType::AMBIENT: *current_stream_ << "return light_colour * fragment_colour;"; break; + case LightType::DIRECTIONAL: + *current_stream_ << "float3 light_dir = "; + *current_stream_ + << (node.normal_input() == nullptr ? "normalize(-light_position.xyz);\n" + : "normalize(-input.tangent_light_pos.xyz);\n"); + + *current_stream_ << "float shadow = 0.0;\n"; + *current_stream_ << + R"(shadow = calculate_shadow(n, input.frag_pos_light_space, light_dir, g_shadow_map); + )"; + + *current_stream_ << R"( + float diff = (1.0 - shadow) * max(dot(n, light_dir), 0.0); + float3 diffuse = {diff, diff, diff}; + + return float4(diffuse * fragment_colour, 1.0); + )"; + break; + case LightType::POINT: + *current_stream_ << "float3 light_dir = "; + *current_stream_ + << (node.normal_input() == nullptr ? "normalize(light_position.xyz - " + "input.frag_position.xyz);\n" + : "normalize(input.tangent_light_pos.xyz - " + "input.tangent_frag_pos.xyz);\n"); + *current_stream_ << R"( + float distance = length(light_position.xyz - input.frag_position.xyz); + float constant = light_attenuation.x; + float linear_term = light_attenuation.y; + float quadratic = light_attenuation.z; + float attenuation = 1.0 / (constant + linear_term * distance + quadratic * (distance * distance)); + float3 att = {attenuation, attenuation, attenuation}; + + float diff = max(dot(n, light_dir), 0.0); + float3 diffuse = {diff, diff, diff}; + + return float4(diffuse * light_colour.xyz * fragment_colour.xyz * att, 1.0); + )"; + break; + default: throw Exception("unknown light type"); + } + + *current_stream_ << "}"; +} + +void HLSLShaderCompiler::visit(const PostProcessingNode &node) +{ + current_stream_ = &vertex_stream_; + current_functions_ = &vertex_functions_; + + // build vertex shader + + *current_stream_ << R"( +PSInput main( + float4 position : TEXCOORD0, + float4 normal : TEXCOORD1, + float4 colour : TEXCOORD2, + float4 tex_coord : TEXCOORD3, + float4 tangent : TEXCOORD4, + float4 bitangent : TEXCOORD5, + uint4 bone_ids : TEXCOORD6, + float4 bone_weights : TEXCOORD7) +{ + matrix bone_transform = mul(bones[bone_ids[0]], bone_weights[0]); + bone_transform += mul(bones[bone_ids[1]], bone_weights[1]); + bone_transform += mul(bones[bone_ids[2]], bone_weights[2]); + bone_transform += mul(bones[bone_ids[3]], bone_weights[3]); + + PSInput result; + + result.frag_position = mul(position, bone_transform); + result.frag_position = mul(result.frag_position, model); + result.position = mul(result.frag_position, view); + result.position = mul(result.position, projection); + result.colour = colour; + result.tex_coord = tex_coord; + + return result; +})"; + + current_stream_ = &fragment_stream_; + current_functions_ = &fragment_functions_; + + current_functions_->emplace(shadow_function); + + // build fragment shader + + *current_stream_ << R"( +float4 main(PSInput input) : SV_TARGET +{)"; + + build_fragment_colour(*current_stream_, node.colour_input(), this); + + *current_stream_ << R"( + float3 mapped = fragment_colour.rgb / (fragment_colour.rgb + float3(1.0, 1.0, 1.0)); + mapped = pow(mapped, float3(1.0 / 2.2, 1.0 / 2.2, 1.0 / 2.2)); + + return float4(mapped.r, mapped.g, mapped.b, 1.0); + })"; +} + +void HLSLShaderCompiler::visit(const ColourNode &node) +{ + const auto colour = node.colour(); + *current_stream_ << "float4(" << colour.r << ", " << colour.g << ", " << colour.b << ", " << colour.a << ")"; +} + +void HLSLShaderCompiler::visit(const TextureNode &node) +{ + const auto id = texture_id(node.texture(), textures_); + + *current_stream_ << "g_texture" << id << ".Sample(g_sampler,"; + + if (node.texture()->flip()) + { + *current_stream_ << "float2(input.tex_coord.x, 1.0 - input.tex_coord.y))"; + } + else + { + *current_stream_ << " input.tex_coord)"; + } +} + +void HLSLShaderCompiler::visit(const InvertNode &node) +{ + current_functions_->emplace(invert_function); + + *current_stream_ << "invert("; + node.input_node()->accept(*this); + *current_stream_ << ")"; +} + +void HLSLShaderCompiler::visit(const BlurNode &node) +{ + const auto id = texture_id(node.input_node()->texture(), textures_); + + current_functions_->emplace(blur_function); + + *current_stream_ << "blur(g_texture" << id << ","; + if (node.input_node()->texture()->flip()) + { + *current_stream_ << "float2(input.tex_coord.x, 1.0 - input.tex_coord.y))"; + } + else + { + *current_stream_ << " input.tex_coord)"; + } +} + +void HLSLShaderCompiler::visit(const CompositeNode &node) +{ + current_functions_->emplace(composite_function); + + *current_stream_ << "composite("; + node.colour1()->accept(*this); + *current_stream_ << ", "; + node.colour2()->accept(*this); + *current_stream_ << ", "; + node.depth1()->accept(*this); + *current_stream_ << ", "; + node.depth2()->accept(*this); + *current_stream_ << ", input.tex_coord)"; +} + +void HLSLShaderCompiler::visit(const VertexPositionNode &) +{ +} + +void HLSLShaderCompiler::visit(const ValueNode &node) +{ + *current_stream_ << std::to_string(node.value()); +} + +void HLSLShaderCompiler::visit(const ValueNode &node) +{ + *current_stream_ << "float3(" << node.value().x << ", " << node.value().y << ", " << node.value().z << ")"; +} + +void HLSLShaderCompiler::visit(const ValueNode &node) +{ + *current_stream_ << "float4(" << node.value().g << ", " << node.value().g << ", " << node.value().b << ", " + << node.value().a << ")"; +} + +void HLSLShaderCompiler::visit(const ArithmeticNode &node) +{ + *current_stream_ << "("; + if (node.arithmetic_operator() == ArithmeticOperator::DOT) + { + *current_stream_ << "dot("; + node.value1()->accept(*this); + *current_stream_ << ", "; + node.value2()->accept(*this); + *current_stream_ << ")"; + } + else + { + node.value1()->accept(*this); + switch (node.arithmetic_operator()) + { + case ArithmeticOperator::ADD: *current_stream_ << " + "; break; + case ArithmeticOperator::SUBTRACT: *current_stream_ << " - "; break; + case ArithmeticOperator::MULTIPLY: *current_stream_ << " * "; break; + case ArithmeticOperator::DIVIDE: *current_stream_ << " / "; break; + default: throw Exception("unknown arithmetic operator"); + } + node.value2()->accept(*this); + } + + *current_stream_ << ")"; +} + +void HLSLShaderCompiler::visit(const ConditionalNode &node) +{ + *current_stream_ << "("; + node.input_value1()->accept(*this); + + switch (node.conditional_operator()) + { + case ConditionalOperator::GREATER: *current_stream_ << " > "; break; + } + + node.input_value2()->accept(*this); + + *current_stream_ << " ? "; + node.output_value1()->accept(*this); + *current_stream_ << " : "; + node.output_value2()->accept(*this); + *current_stream_ << ")"; +} + +void HLSLShaderCompiler::visit(const ComponentNode &node) +{ + node.input_node()->accept(*this); + *current_stream_ << "." << node.component(); +} + +void HLSLShaderCompiler::visit(const CombineNode &node) +{ + *current_stream_ << "float4("; + node.value1()->accept(*this); + *current_stream_ << ", "; + node.value2()->accept(*this); + *current_stream_ << ", "; + node.value3()->accept(*this); + *current_stream_ << ", "; + node.value4()->accept(*this); + *current_stream_ << ")"; +} + +void HLSLShaderCompiler::visit(const SinNode &node) +{ + *current_stream_ << "sin("; + node.input_node()->accept(*this); + *current_stream_ << ")"; +} + +std::string HLSLShaderCompiler::vertex_shader() const +{ + std::stringstream strm{}; + + for (const auto &function : vertex_functions_) + { + strm << function << '\n'; + } + + strm << uniforms << '\n'; + strm << ps_input << '\n'; + + strm << vertex_stream_.str() << '\n'; + + return strm.str(); +} + +std::string HLSLShaderCompiler::fragment_shader() const +{ + std::stringstream strm{}; + + strm << "SamplerState g_sampler : register(s0);\n"; + strm << "Texture2D g_shadow_map : register(t0);\n"; + + for (auto i = 0u; i < textures_.size(); ++i) + { + strm << "Texture2D g_texture" << i + 1u << " : register(t" << i + 1u << ");\n"; + } + + for (const auto &function : fragment_functions_) + { + strm << function << '\n'; + } + + strm << uniforms << '\n'; + strm << ps_input << '\n'; + strm << fragment_stream_.str() << '\n'; + + return strm.str(); +} + +std::vector HLSLShaderCompiler::textures() const +{ + return textures_; +} +} diff --git a/src/graphics/ios/CMakeLists.txt b/src/graphics/ios/CMakeLists.txt new file mode 100644 index 00000000..ece135fa --- /dev/null +++ b/src/graphics/ios/CMakeLists.txt @@ -0,0 +1,14 @@ +set(DEFAULT_ROOT "${PROJECT_SOURCE_DIR}/src/core/default") +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/graphics/ios") + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/app_delegate.h + ${INCLUDE_ROOT}/ios_window.h + ${INCLUDE_ROOT}/ios_window_manager.h + ${INCLUDE_ROOT}/metal_view.h + ${INCLUDE_ROOT}/metal_view_controller.h + app_delegate.mm + ios_window.mm + ios_window_manager.cpp + metal_view.mm + metal_view_controller.mm) diff --git a/src/graphics/ios/app_delegate.mm b/src/graphics/ios/app_delegate.mm new file mode 100644 index 00000000..6f3475fe --- /dev/null +++ b/src/graphics/ios/app_delegate.mm @@ -0,0 +1,70 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#import "graphics/ios/app_delegate.h" + +#include + +#import +#import +#import +#import +#import + +#include "graphics/ios/metal_view_controller.h" +#include "log/log.h" + +namespace iris +{ + +// globals for calling back into game +extern std::function g_entry; +extern int g_argc; +extern char **g_argv; + +} + +@interface AppDelegate () + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + LOG_ENGINE_INFO("AppDelegate", "setting up window and view"); + + // create a new window the size of the screen + self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; + + // create metal view controller + auto *view_controller = [[MetalViewController alloc] init]; + + // make window visible + [[self window] makeKeyAndVisible]; + [[self window] setRootViewController:view_controller]; + [[self window] setNeedsDisplay]; + + // fire of a call so the game entry point gets called + // this has to be done via a selector as didFinishLaunchingWithOptions + // must return, or else nothing will get rendered + [self performSelector:@selector(callEntry) withObject:nil afterDelay:0.0]; + + LOG_ENGINE_INFO("AppDelegate", "launch done"); + + return YES; +} + +- (void)callEntry +{ + LOG_ENGINE_INFO("AppDelegate", "calling main"); + + iris::g_entry(iris::g_argc, iris::g_argv); + + LOG_ENGINE_INFO("AppDelegate", "main done"); +} + +@end diff --git a/src/graphics/ios/ios_window.mm b/src/graphics/ios/ios_window.mm new file mode 100644 index 00000000..bf87d7c5 --- /dev/null +++ b/src/graphics/ios/ios_window.mm @@ -0,0 +1,70 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/ios/ios_window.h" + +#include +#include + +#import + +#import "graphics/ios/metal_view_controller.h" +#include "graphics/metal/metal_renderer.h" +#include "log/log.h" + +namespace iris +{ + +IOSWindow::IOSWindow(std::uint32_t width, std::uint32_t height) + : Window(width, height) +{ + const auto bounds = [[UIScreen mainScreen] bounds]; + width_ = bounds.size.width; + height_ = bounds.size.height; + + renderer_ = std::make_unique(width_, height_); +} + +std::optional IOSWindow::pump_event() +{ + const CFTimeInterval seconds = 0.000002; + + // run the default loop, this pumps touch events which will then be picked + // up by our view + auto result = kCFRunLoopRunHandledSource; + do + { + result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, seconds, TRUE); + } while (result == kCFRunLoopRunHandledSource); + + const auto *window = [[[UIApplication sharedApplication] windows] objectAtIndex:0]; + const auto *root_view_controller = static_cast([window rootViewController]); + + std::optional event; + + // get next event from view (if one is available) + if (!root_view_controller->events_.empty()) + { + event = root_view_controller->events_.front(); + root_view_controller->events_.pop(); + } + + return event; +} + +std::uint32_t IOSWindow::screen_scale() const +{ + static std::uint32_t scale = 0u; + + if (scale == 0u) + { + scale = static_cast([[UIScreen mainScreen] scale]); + } + + return scale; +} + +} diff --git a/src/graphics/ios/ios_window_manager.cpp b/src/graphics/ios/ios_window_manager.cpp new file mode 100644 index 00000000..61f57e2a --- /dev/null +++ b/src/graphics/ios/ios_window_manager.cpp @@ -0,0 +1,31 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/ios/ios_window_manager.h" + +#include +#include + +#include "core/error_handling.h" +#include "graphics/ios/ios_window.h" + +namespace iris +{ + +Window *IOSWindowManager::create_window(std::uint32_t width, std::uint32_t height) +{ + ensure(!current_window_, "window already created"); + + current_window_ = std::make_unique(width, height); + return current_window_.get(); +} + +Window *IOSWindowManager::current_window() const +{ + return current_window_.get(); +} + +} diff --git a/src/graphics/ios/metal_view.mm b/src/graphics/ios/metal_view.mm new file mode 100644 index 00000000..fe528ae2 --- /dev/null +++ b/src/graphics/ios/metal_view.mm @@ -0,0 +1,47 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#import "graphics/ios/metal_view.h" + +#include + +#import +#import + +#include "core/root.h" +#include "graphics/ios/ios_window.h" +#include "graphics/ios/ios_window_manager.h" + +@implementation MetalView + +CADisplayLink *_displayLink; + ++ (id)layerClass +{ + // we need to change the backing layer so metal can be used + return [CAMetalLayer class]; +} + +- (instancetype)init +{ + if ((self = [super initWithFrame:[[UIScreen mainScreen] bounds]])) + { + // basic metal setup + _metalLayer = (CAMetalLayer *)[self layer]; + _device = MTLCreateSystemDefaultDevice(); + _metalLayer.device = _device; + _metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm; + _metalLayer.frame = self.layer.frame; + self.contentScaleFactor = iris::Root::window_manager().current_window()->screen_scale(); + [self setUserInteractionEnabled:YES]; + [super setUserInteractionEnabled:YES]; + [self setBackgroundColor:[UIColor redColor]]; + } + + return self; +} + +@end diff --git a/src/graphics/ios/metal_view_controller.mm b/src/graphics/ios/metal_view_controller.mm new file mode 100644 index 00000000..2e8416f6 --- /dev/null +++ b/src/graphics/ios/metal_view_controller.mm @@ -0,0 +1,73 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#import "graphics/ios/metal_view_controller.h" + +#include +#include + +#import "graphics/ios/metal_view.h" + +@interface MetalViewController () +@end + +@implementation MetalViewController + +- (void)viewDidLoad +{ + [super viewDidLoad]; + [[self view] setMultipleTouchEnabled:YES]; +} + +- (void)viewDidAppear:(BOOL)animated +{ + [super viewDidAppear:animated]; + NSNumber *value = [NSNumber numberWithInt:UIInterfaceOrientationLandscapeLeft]; + [[UIDevice currentDevice] setValue:value forKey:@"orientation"]; +} + +- (void)loadView +{ + MetalView *view = [[MetalView alloc] init]; + [self setView:view]; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + for (UITouch *touch in touches) + { + CGPoint touchPosition = [touch locationInView:[touch window]]; + events_.emplace(iris::TouchEvent( + reinterpret_cast(touch), iris::TouchType::BEGIN, touchPosition.x, touchPosition.y)); + } +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + for (UITouch *touch in touches) + { + CGPoint touchPosition = [touch locationInView:[touch window]]; + events_.emplace(iris::TouchEvent( + reinterpret_cast(touch), iris::TouchType::MOVE, touchPosition.x, touchPosition.y)); + } +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + for (UITouch *touch in touches) + { + CGPoint touchPosition = [touch locationInView:[touch window]]; + events_.emplace(iris::TouchEvent( + reinterpret_cast(touch), iris::TouchType::END, touchPosition.x, touchPosition.y)); + } +} + +- (BOOL)prefersStatusBarHidden +{ + return YES; +} + +@end diff --git a/src/graphics/lights/CMakeLists.txt b/src/graphics/lights/CMakeLists.txt new file mode 100644 index 00000000..d3f96853 --- /dev/null +++ b/src/graphics/lights/CMakeLists.txt @@ -0,0 +1,12 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/graphics/lights") + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/ambient_light.h + ${INCLUDE_ROOT}/directional_light.h + ${INCLUDE_ROOT}/light.h + ${INCLUDE_ROOT}/light_type.h + ${INCLUDE_ROOT}/lighting_rig.h + ${INCLUDE_ROOT}/point_light.h + ambient_light.cpp + directional_light.cpp + point_light.cpp) diff --git a/src/graphics/lights/ambient_light.cpp b/src/graphics/lights/ambient_light.cpp new file mode 100644 index 00000000..d4fe04c9 --- /dev/null +++ b/src/graphics/lights/ambient_light.cpp @@ -0,0 +1,69 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/lights/ambient_light.h" + +#include +#include + +#include "core/colour.h" +#include "graphics/lights/light.h" +#include "graphics/lights/light_type.h" + +namespace iris +{ + +AmbientLight::AmbientLight(const Colour &colour) + : colour_(colour) +{ +} + +LightType AmbientLight::type() const +{ + return LightType::AMBIENT; +} + +std::array AmbientLight::colour_data() const +{ + std::array light_data{}; + light_data.fill(0.0f); + + // sanity check we have enough space + static_assert(light_data.size() * sizeof(decltype(light_data)::value_type) >= sizeof(colour_)); + + // copy light data straight into buffer + std::memcpy(light_data.data(), &colour_, sizeof(colour_)); + + return light_data; +} + +std::array AmbientLight::world_space_data() const +{ + std::array light_data{}; + light_data.fill(0.0f); + + return light_data; +} + +std::array AmbientLight::attenuation_data() const +{ + std::array light_data{}; + light_data.fill(0.0f); + + return light_data; +} + +Colour AmbientLight::colour() const +{ + return colour_; +} + +void AmbientLight::set_colour(const Colour &colour) +{ + colour_ = colour; +} + +} diff --git a/src/graphics/lights/directional_light.cpp b/src/graphics/lights/directional_light.cpp new file mode 100644 index 00000000..d93ebdec --- /dev/null +++ b/src/graphics/lights/directional_light.cpp @@ -0,0 +1,82 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/lights/directional_light.h" + +#include +#include + +#include "core/camera.h" +#include "core/matrix4.h" +#include "core/vector3.h" +#include "graphics/lights/light_type.h" + +namespace iris +{ + +DirectionalLight::DirectionalLight(const Vector3 &direction, bool cast_shadows) + : direction_(direction) + , shadow_camera_(CameraType::ORTHOGRAPHIC, 100u, 100u, 1000u) + , cast_shadows_(cast_shadows) +{ + shadow_camera_.set_view(Matrix4::make_look_at(-direction_, {}, {0.0f, 1.0f, 0.0f})); +} + +LightType DirectionalLight::type() const +{ + return LightType::DIRECTIONAL; +} + +std::array DirectionalLight::colour_data() const +{ + std::array light_data{}; + light_data.fill(1.0f); + + return light_data; +} + +std::array DirectionalLight::world_space_data() const +{ + std::array light_data{}; + light_data.fill(0.0f); + + static_assert(light_data.size() * sizeof(decltype(light_data)::value_type) >= sizeof(direction_)); + + std::memcpy(light_data.data(), &direction_, sizeof(direction_)); + + return light_data; +} + +std::array DirectionalLight::attenuation_data() const +{ + std::array light_data{}; + light_data.fill(1.0f); + + return light_data; +} + +Vector3 DirectionalLight::direction() const +{ + return direction_; +} + +void DirectionalLight::set_direction(const Vector3 &direction) +{ + direction_ = direction; + shadow_camera_.set_view(Matrix4::make_look_at(-direction_, {}, {0.0f, 1.0f, 0.0f})); +} + +bool DirectionalLight::casts_shadows() const +{ + return cast_shadows_; +} + +const Camera &DirectionalLight::shadow_camera() const +{ + return shadow_camera_; +} + +} diff --git a/src/graphics/lights/point_light.cpp b/src/graphics/lights/point_light.cpp new file mode 100644 index 00000000..ded5ca20 --- /dev/null +++ b/src/graphics/lights/point_light.cpp @@ -0,0 +1,130 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/lights/point_light.h" + +#include +#include + +#include "core/vector3.h" +#include "graphics/lights/light_type.h" + +namespace iris +{ + +PointLight::PointLight(const Vector3 &position) + : PointLight(position, {1.0f, 1.0f, 1.0f, 1.0f}) +{ +} + +PointLight::PointLight(const Vector3 &position, const Colour &colour) + : position_(position) + , colour_(colour) + , attenuation_terms_() +{ + attenuation_terms_.constant = 0.0f; + attenuation_terms_.linear = 1.0f; + attenuation_terms_.quadratic = 0.0f; +} + +LightType PointLight::type() const +{ + return LightType::POINT; +} + +std::array PointLight::colour_data() const +{ + std::array light_data{}; + light_data.fill(0.0f); + + // sanity check we have enough space + static_assert(light_data.size() * sizeof(decltype(light_data)::value_type) >= sizeof(colour_)); + + // copy light data straight into buffer + std::memcpy(light_data.data(), &colour_, sizeof(colour_)); + + return light_data; +} + +std::array PointLight::world_space_data() const +{ + std::array light_data{}; + light_data.fill(0.0f); + + // sanity check we have enough space + static_assert(light_data.size() * sizeof(decltype(light_data)::value_type) >= sizeof(position_)); + + // copy light data straight into buffer + std::memcpy(light_data.data(), &position_, sizeof(position_)); + + return light_data; +} + +std::array PointLight::attenuation_data() const +{ + std::array light_data{}; + light_data.fill(0.0f); + + // sanity check we have enough space + static_assert(light_data.size() * sizeof(decltype(light_data)::value_type) == sizeof(attenuation_terms_)); + + // copy light data straight into buffer + std::memcpy(light_data.data(), &attenuation_terms_, sizeof(attenuation_terms_)); + + return light_data; +} + +Vector3 PointLight::position() const +{ + return position_; +} + +void PointLight::set_position(const Vector3 &position) +{ + position_ = position; +} + +Colour PointLight::colour() const +{ + return colour_; +} + +void PointLight::set_colour(const Colour &colour) +{ + colour_ = colour; +} + +float PointLight::attenuation_constant_term() const +{ + return attenuation_terms_.constant; +} + +void PointLight::set_attenuation_constant_term(float constant) +{ + attenuation_terms_.constant = constant; +} + +float PointLight::attenuation_linear_term() const +{ + return attenuation_terms_.linear; +} + +void PointLight::set_attenuation_linear_term(float linear) +{ + attenuation_terms_.linear = linear; +} + +float PointLight::attenuation_quadratic_term() const +{ + return attenuation_terms_.quadratic; +} + +void PointLight::set_attenuation_quadratic_term(float quadratic) +{ + attenuation_terms_.quadratic = quadratic; +} + +} diff --git a/src/graphics/macos/CMakeLists.txt b/src/graphics/macos/CMakeLists.txt new file mode 100644 index 00000000..1ff14f50 --- /dev/null +++ b/src/graphics/macos/CMakeLists.txt @@ -0,0 +1,16 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/graphics/macos") + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/macos_window.h + ${INCLUDE_ROOT}/macos_window_manager.h + ${INCLUDE_ROOT}/metal_app_delegate.h + ${INCLUDE_ROOT}/metal_view.h + ${INCLUDE_ROOT}/opengl_app_delegate.h + ${INCLUDE_ROOT}/opengl_view.h + macos_window.mm + macos_window_manager.cpp + metal_app_delegate.m + metal_view.m + opengl_app_delegate.m + opengl_view.m + text_factory.mm) diff --git a/src/graphics/macos/macos_window.mm b/src/graphics/macos/macos_window.mm new file mode 100644 index 00000000..9912072d --- /dev/null +++ b/src/graphics/macos/macos_window.mm @@ -0,0 +1,298 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/macos/macos_window.h" + +#include + +#import +#import + +#include +#include + +#include "core/error_handling.h" +#include "core/root.h" +#include "events/keyboard_event.h" +#include "events/mouse_button_event.h" +#include "events/mouse_event.h" +#include "graphics/macos/metal_app_delegate.h" +#include "graphics/macos/opengl_app_delegate.h" +#include "graphics/metal/metal_renderer.h" +#include "graphics/opengl/opengl_renderer.h" +#include "graphics/render_target.h" +#include "log/log.h" + +namespace +{ + +/** + * Helper function to convert an OS X Key code to an engine specific key. + * + * @param key_code + * OS X specific Key code. + * + * @returns + * Emgine specific Key representation. + */ +iris::Key macos_key_to_engine_Key(const std::uint16_t key_code) +{ + iris::Key key; + + switch (key_code) + { + case 0x00: key = iris::Key::A; break; + case 0x01: key = iris::Key::S; break; + case 0x02: key = iris::Key::D; break; + case 0x03: key = iris::Key::F; break; + case 0x04: key = iris::Key::H; break; + case 0x05: key = iris::Key::G; break; + case 0x06: key = iris::Key::Z; break; + case 0x07: key = iris::Key::X; break; + case 0x08: key = iris::Key::C; break; + case 0x09: key = iris::Key::V; break; + case 0x0B: key = iris::Key::B; break; + case 0x0C: key = iris::Key::Q; break; + case 0x0D: key = iris::Key::W; break; + case 0x0E: key = iris::Key::E; break; + case 0x0F: key = iris::Key::R; break; + case 0x10: key = iris::Key::Y; break; + case 0x11: key = iris::Key::T; break; + case 0x12: key = iris::Key::NUM_1; break; + case 0x13: key = iris::Key::NUM_2; break; + case 0x14: key = iris::Key::NUM_3; break; + case 0x15: key = iris::Key::NUM_4; break; + case 0x16: key = iris::Key::NUM_6; break; + case 0x17: key = iris::Key::NUM_5; break; + case 0x18: key = iris::Key::EQUAL; break; + case 0x19: key = iris::Key::NUM_9; break; + case 0x1A: key = iris::Key::NUM_7; break; + case 0x1B: key = iris::Key::MINUS; break; + case 0x1C: key = iris::Key::NUM_8; break; + case 0x1D: key = iris::Key::NUM_0; break; + case 0x1E: key = iris::Key::RIGHT_BRACKET; break; + case 0x1F: key = iris::Key::O; break; + case 0x20: key = iris::Key::U; break; + case 0x21: key = iris::Key::LEFT_BRACKET; break; + case 0x22: key = iris::Key::I; break; + case 0x23: key = iris::Key::P; break; + case 0x24: key = iris::Key::RETURN; break; + case 0x25: key = iris::Key::L; break; + case 0x26: key = iris::Key::J; break; + case 0x27: key = iris::Key::QUOTE; break; + case 0x28: key = iris::Key::K; break; + case 0x29: key = iris::Key::SEMI_COLON; break; + case 0x2A: key = iris::Key::BACKSLASH; break; + case 0x2B: key = iris::Key::COMMA; break; + case 0x2C: key = iris::Key::SLASH; break; + case 0x2D: key = iris::Key::N; break; + case 0x2E: key = iris::Key::M; break; + case 0x2F: key = iris::Key::PERIOD; break; + case 0x30: key = iris::Key::TAB; break; + case 0x31: key = iris::Key::SPACE; break; + case 0x32: key = iris::Key::GRAVE; break; + case 0x33: key = iris::Key::FORWARD_DELETE; break; + case 0x35: key = iris::Key::ESCAPE; break; + case 0x37: key = iris::Key::COMMAND; break; + case 0x38: key = iris::Key::SHIFT; break; + case 0x39: key = iris::Key::CAPS_LOCK; break; + case 0x3A: key = iris::Key::OPTION; break; + case 0x3B: key = iris::Key::CONTROL; break; + case 0x3C: key = iris::Key::RIGHT_SHIFT; break; + case 0x3D: key = iris::Key::RIGHT_OPTION; break; + case 0x3E: key = iris::Key::RIGHT_CONTROL; break; + case 0x3F: key = iris::Key::FUNCTION; break; + case 0x40: key = iris::Key::F17; break; + case 0x41: key = iris::Key::KEYPAD_DECIMAL; break; + case 0x43: key = iris::Key::KEYPAD_MULTIPLY; break; + case 0x45: key = iris::Key::KEYPAD_PLUS; break; + case 0x47: key = iris::Key::KEYPAD_CLEAR; break; + case 0x48: key = iris::Key::VOLUME_UP; break; + case 0x49: key = iris::Key::VOLUME_DOWN; break; + case 0x4A: key = iris::Key::MUTE; break; + case 0x4B: key = iris::Key::KEYPAD_DIVIDE; break; + case 0x4C: key = iris::Key::KEYPAD_ENTER; break; + case 0x4E: key = iris::Key::KEYPAD_MINUS; break; + case 0x4F: key = iris::Key::F18; break; + case 0x50: key = iris::Key::F19; break; + case 0x51: key = iris::Key::KEYPAD_EQUALS; break; + case 0x52: key = iris::Key::KEYPAD_0; break; + case 0x53: key = iris::Key::KEYPAD_1; break; + case 0x54: key = iris::Key::KEYPAD_2; break; + case 0x55: key = iris::Key::KEYPAD_3; break; + case 0x56: key = iris::Key::KEYPAD_4; break; + case 0x57: key = iris::Key::KEYPAD_5; break; + case 0x58: key = iris::Key::KEYPAD_6; break; + case 0x59: key = iris::Key::KEYPAD_7; break; + case 0x5A: key = iris::Key::F20; break; + case 0x5B: key = iris::Key::KEYPAD_8; break; + case 0x5C: key = iris::Key::KEYPAD_9; break; + case 0x60: key = iris::Key::F5; break; + case 0x61: key = iris::Key::F6; break; + case 0x62: key = iris::Key::F7; break; + case 0x63: key = iris::Key::F3; break; + case 0x64: key = iris::Key::F8; break; + case 0x65: key = iris::Key::F9; break; + case 0x67: key = iris::Key::F11; break; + case 0x69: key = iris::Key::F13; break; + case 0x6A: key = iris::Key::F16; break; + case 0x6B: key = iris::Key::F14; break; + case 0x6D: key = iris::Key::F10; break; + case 0x6F: key = iris::Key::F12; break; + case 0x71: key = iris::Key::F15; break; + case 0x72: key = iris::Key::HELP; break; + case 0x73: key = iris::Key::HOME; break; + case 0x74: key = iris::Key::PAGE_UP; break; + case 0x75: key = iris::Key::FORWARD_DELETE; break; + case 0x76: key = iris::Key::F4; break; + case 0x77: key = iris::Key::END; break; + case 0x78: key = iris::Key::F2; break; + case 0x79: key = iris::Key::PAGE_DOWN; break; + case 0x7A: key = iris::Key::F1; break; + case 0x7B: key = iris::Key::LEFT_ARROW; break; + case 0x7C: key = iris::Key::RIGHT_ARROW; break; + case 0x7D: key = iris::Key::DOWN_ARROW; break; + case 0x7E: key = iris::Key::UP_ARROW; break; + default: key = iris::Key::UNKNOWN; + } + + return key; +} + +/** + * Helper method to handle native keyboard events. + * + * @param event + * Native Event object. + */ +iris::KeyboardEvent handle_keyboard_event(NSEvent *event) +{ + // extract the Key code from the event + const std::uint16_t key_code = [event keyCode]; + + // convert the NSEventType to our Event state + const auto type = ([event type] == NSEventTypeKeyDown) ? iris::KeyState::DOWN : iris::KeyState::UP; + + // convert Key code and dispatch + const auto key = macos_key_to_engine_Key(key_code); + + return {key, type}; +} + +/** + * Helper method to handle native mouse events. + * + * @param event + * Native Event object. + */ +iris::MouseEvent handle_mouse_event(NSEvent *event) +{ + // get mouse delta + std::int32_t dx = 0; + std::int32_t dy = 0; + ::CGGetLastMouseDelta(&dx, &dy); + + // convert and dispatch + return {static_cast(dx), static_cast(dy)}; +} + +} + +namespace iris +{ + +MacosWindow::MacosWindow(std::uint32_t width, std::uint32_t height) + : Window(width, height) +{ + // get and/or create the application singleton + NSApplication *app = [NSApplication sharedApplication]; + + // this is an ordinary app + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + + // make this app the active app + [NSApp activateIgnoringOtherApps:YES]; + + id app_delegate = nullptr; + + const auto api = Root::graphics_api(); + + // create a graphics api specific Renderer and app delegate + if (api == "metal") + { + app_delegate = [[MetalAppDelegate alloc] initWithRect:NSMakeRect(0.0f, 0.0f, width_, height_)]; + renderer_ = std::make_unique(width_, height_); + } + else if (api == "opengl") + { + app_delegate = [[OpenGLAppDelegate alloc] initWithRect:NSMakeRect(0.0f, 0.0f, width_, height_)]; + renderer_ = std::make_unique(width_, height_); + } + else + { + throw Exception("unsupported graphics api"); + } + + // check that we created the delegate + ensure(app_delegate != nil, "failed to create AppDelegate"); + + // set the delegate + [app setDelegate:app_delegate]; + + // activate the app + [app finishLaunching]; + + [NSCursor hide]; + + // opengl window won't render till we pump events, so we do that here as it + // doesn't matter if we are using opengl or metal + pump_event(); + + LOG_ENGINE_INFO("window", "macos window created {} {}", width_, height_); +} + +std::uint32_t MacosWindow::screen_scale() const +{ + auto *window = [[NSApp windows] firstObject]; + return static_cast([[window screen] backingScaleFactor]); +} + +std::optional MacosWindow::pump_event() +{ + std::optional evt{}; + + NSEvent *event = nil; + + // flush next event + event = [NSApp nextEventMatchingMask:NSEventMaskAny + untilDate:[NSDate distantPast] + inMode:NSDefaultRunLoopMode + dequeue:YES]; + + if (event != nil) + { + // handle native event + switch ([event type]) + { + case NSEventTypeKeyDown: [[fallthrough]]; + case NSEventTypeKeyUp: evt = handle_keyboard_event(event); break; + case NSEventTypeMouseMoved: evt = handle_mouse_event(event); break; + case NSEventTypeLeftMouseDown: evt = MouseButtonEvent{MouseButton::LEFT, MouseButtonState::DOWN}; break; + case NSEventTypeLeftMouseUp: evt = MouseButtonEvent{MouseButton::LEFT, MouseButtonState::UP}; break; + case NSEventTypeRightMouseDown: evt = MouseButtonEvent{MouseButton::RIGHT, MouseButtonState::DOWN}; break; + case NSEventTypeRightMouseUp: evt = MouseButtonEvent{MouseButton::RIGHT, MouseButtonState::UP}; break; + default: break; + } + + // dispatch the Event to other objects, this stops us swallowing + // all events and preventing anything else from receiving them + [NSApp sendEvent:event]; + } + + return evt; +} + +} diff --git a/src/graphics/macos/macos_window_manager.cpp b/src/graphics/macos/macos_window_manager.cpp new file mode 100644 index 00000000..6ab090f0 --- /dev/null +++ b/src/graphics/macos/macos_window_manager.cpp @@ -0,0 +1,31 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/macos/macos_window_manager.h" + +#include +#include + +#include "core/error_handling.h" +#include "graphics/macos/macos_window.h" + +namespace iris +{ + +Window *MacosWindowManager::create_window(std::uint32_t width, std::uint32_t height) +{ + ensure(!current_window_, "window already created"); + + current_window_ = std::make_unique(width, height); + return current_window_.get(); +} + +Window *MacosWindowManager::current_window() const +{ + return current_window_.get(); +} + +} diff --git a/src/graphics/macos/metal_app_delegate.m b/src/graphics/macos/metal_app_delegate.m new file mode 100644 index 00000000..05c5eae1 --- /dev/null +++ b/src/graphics/macos/metal_app_delegate.m @@ -0,0 +1,87 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#import "graphics/macos/metal_app_delegate.h" + +#import +#import +#import +#import +#import +#import + +#include "graphics/macos/metal_view.h" + +@implementation MetalAppDelegate + +- (id)initWithRect:(NSRect)rect +{ + // call the super init, perform custom initialisation if this succeeds + if (self = [super init]) + { + [self setWidth:rect.size.width]; + [self setHeight:rect.size.height]; + + MTLCreateSystemDefaultDevice(); + + // create our window. It should have a title and render all content + // to a buffer before being flushed, do not defer the creation of the + // window + NSWindow *window = [[NSWindow alloc] initWithContentRect:rect + styleMask:NSWindowStyleMaskTitled + backing:NSBackingStoreBuffered + defer:NO]; + + // create and setup a metal view + MetalView *view = [[MetalView alloc] initWithFrame:rect device:MTLCreateSystemDefaultDevice()]; + [view setClearColor:MTLClearColorMake(0, 0, 0, 1)]; + [view setColorPixelFormat:MTLPixelFormatRGBA16Float]; + [view setDepthStencilPixelFormat:MTLPixelFormatDepth32Float]; + + CAMetalLayer *layer = (CAMetalLayer *)view.layer; + + const CFStringRef name = kCGColorSpaceExtendedSRGB; + CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(name); + layer.colorspace = colorspace; + layer.wantsExtendedDynamicRangeContent = YES; + + // add the view to the window + [window setContentView:view]; + + // centre the window on the screen + [window center]; + + // release window when it is closed + [window setReleasedWhenClosed:YES]; + + // show the window + [window makeKeyAndOrderFront:self]; + + [window makeFirstResponder:view]; + + [window setColorSpace:[NSColorSpace genericRGBColorSpace]]; + + // redraw the view before displaying + [view setNeedsDisplay:YES]; + + // create a tracking area the size of the screen + NSTrackingArea *tracking = [[NSTrackingArea alloc] initWithRect:rect + options:NSTrackingMouseMoved | NSTrackingActiveAlways + owner:view + userInfo:nil]; + + // add the tracking area + [view addTrackingArea:tracking]; + + // hide and lock the cursor + CGDisplayHideCursor(kCGDirectMainDisplay); + CGAssociateMouseAndMouseCursorPosition(NO); + } + + return self; +} + +@end diff --git a/src/graphics/macos/metal_view.m b/src/graphics/macos/metal_view.m new file mode 100644 index 00000000..a4837933 --- /dev/null +++ b/src/graphics/macos/metal_view.m @@ -0,0 +1,26 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#import "graphics/macos/metal_view.h" + +@implementation MetalView +{ +} + +- (BOOL)acceptsFirstResponder +{ + // make sure we receive key events + return YES; +} + +- (void)keyDown:(NSEvent *)theEvent +{ + // once we receive a key event we completely ignore it! This is because we + // are handling these elsewhere, but having the view accept and ignore key + // events prevents the annoying OS X doonk noise +} + +@end diff --git a/src/graphics/macos/opengl_app_delegate.m b/src/graphics/macos/opengl_app_delegate.m new file mode 100644 index 00000000..cabc0596 --- /dev/null +++ b/src/graphics/macos/opengl_app_delegate.m @@ -0,0 +1,98 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#import "graphics/macos/opengl_app_delegate.h" + +#import +#import +#import + +#import "graphics/macos/opengl_view.h" + +@implementation OpenGLAppDelegate + +- (id)initWithRect:(NSRect)rect +{ + // call the super init, perform custom initialisation if this succeeds + if (self = [super init]) + { + [self setWidth:rect.size.width]; + [self setHeight:rect.size.height]; + + // create our window. It should have a title and render all content + // to a buffer before being flushed, do not defer the creation of the + // window + NSWindow *window = [[NSWindow alloc] initWithContentRect:rect + styleMask:NSWindowStyleMaskTitled + backing:NSBackingStoreBuffered + defer:NO]; + + // here we specify the attributes of the OpenGl view. + NSOpenGLPixelFormatAttribute pixelFormatAttributes[] = { + NSOpenGLPFAOpenGLProfile, + NSOpenGLProfileVersion3_2Core, // use at least OpenGl 3.2 + NSOpenGLPFAColorSize, + 32, // 32 bit colour + NSOpenGLPFAAlphaSize, + 8, // 8 bit alpha + NSOpenGLPFADepthSize, + 32, // 32 bit depth buffer + NSOpenGLPFADoubleBuffer, // use double buffering + NSOpenGLPFAAccelerated, // use hardware acceleration + 0 // array termination + }; + + // create the pixel format object with the above attributes + NSOpenGLPixelFormat *pixel_format = [[NSOpenGLPixelFormat alloc] initWithAttributes:pixelFormatAttributes]; + + // create our OpenGl view, make it the same size as the window + OpenGLView *view = [[OpenGLView alloc] initWithFrame:rect pixelFormat:pixel_format]; + + // ensure OpenGL fully utilises retina displays + [view setWantsBestResolutionOpenGLSurface:YES]; + + // add the view to the window + [window setContentView:view]; + + // centre the window on the screen + [window center]; + + // release window when it is closed + [window setReleasedWhenClosed:YES]; + + // show the window + [window makeKeyAndOrderFront:self]; + + // make this the current OpenGl context + [[view openGLContext] makeCurrentContext]; + + // redraw the view before displaying + [view setNeedsDisplay:YES]; + + [window makeFirstResponder:view]; + + // create a tracking area the size of the screen + NSTrackingArea *tracking = [[NSTrackingArea alloc] initWithRect:rect + options:NSTrackingMouseMoved | NSTrackingActiveAlways + owner:view + userInfo:nil]; + + // add the tracking area + [view addTrackingArea:tracking]; + + // hide the cursor + CGDisplayHideCursor(kCGDirectMainDisplay); + CGAssociateMouseAndMouseCursorPosition(NO); + + // disable vsync + int value = 0; + CGLSetParameter(CGLGetCurrentContext(), kCGLCPSwapInterval, &value); + } + + return self; +} + +@end diff --git a/src/graphics/macos/opengl_view.m b/src/graphics/macos/opengl_view.m new file mode 100644 index 00000000..41c27673 --- /dev/null +++ b/src/graphics/macos/opengl_view.m @@ -0,0 +1,26 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#import "graphics/macos/opengl_view.h" + +@implementation OpenGLView +{ +} + +- (BOOL)acceptsFirstResponder +{ + // make sure we receive key events + return YES; +} + +- (void)keyDown:(NSEvent *)theEvent +{ + // once we receive a key event we completely ignore it! This is because we + // are handling these elsewhere, but having the view accept and ignore key + // events prevents the annoying macos doonk noise +} + +@end diff --git a/src/graphics/macos/text_factory.mm b/src/graphics/macos/text_factory.mm new file mode 100644 index 00000000..179db8c9 --- /dev/null +++ b/src/graphics/macos/text_factory.mm @@ -0,0 +1,140 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/text_factory.h" + +#include +#include +#include +#include + +//#import +#import +#import +#import + +#include "core/auto_release.h" +#include "core/colour.h" +#include "core/data_buffer.h" +#include "core/error_handling.h" +#include "core/root.h" +#include "graphics/texture.h" +#include "graphics/texture_manager.h" +#include "log/log.h" + +namespace iris::text_factory +{ + +Texture *create(const std::string &font_name, const std::uint32_t size, const std::string &text, const Colour &colour) +{ + // create a CoreFoundation string object from supplied Font name + const auto font_name_cf = AutoRelease{ + ::CFStringCreateWithCString(kCFAllocatorDefault, font_name.c_str(), kCFStringEncodingASCII), ::CFRelease}; + + expect(font_name_cf, "failed to create CF string"); + + // create Font object + AutoRelease font = { + ::CTFontCreateWithName(font_name_cf.get(), static_cast(size), nullptr), ::CFRelease}; + + ensure(font, "failed to create font"); + + // create a device dependant colour space + AutoRelease colour_space = {::CGColorSpaceCreateDeviceRGB(), ::CGColorSpaceRelease}; + + expect(colour_space, "failed to create colour space"); + + // create a CoreFoundation colour object from supplied colour + const CGFloat components[] = {colour.r, colour.g, colour.b, colour.a}; + AutoRelease font_colour = {::CGColorCreate(colour_space.get(), components), ::CGColorRelease}; + + expect(font_colour, "failed to create colour"); + + std::array keys = {{kCTFontAttributeName, kCTForegroundColorAttributeName}}; + std::array values = {{font.get(), font_colour.get()}}; + + // create string attributes dictionary, containing Font name and colour + AutoRelease attributes = { + ::CFDictionaryCreate( + kCFAllocatorDefault, + reinterpret_cast(keys.data()), + reinterpret_cast(values.data()), + keys.size(), + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks), + ::CFRelease}; + + expect(attributes, "failed to create attributes"); + + LOG_DEBUG("font", "creating sprites for string: {}", text); + + // create CoreFoundation string object from supplied text + const auto text_cf = AutoRelease{ + ::CFStringCreateWithCString(kCFAllocatorDefault, text.c_str(), kCFStringEncodingASCII), ::CFRelease}; + + // create a CoreFoundation attributed string object + const auto attr_string = AutoRelease{ + ::CFAttributedStringCreate(kCFAllocatorDefault, text_cf.get(), attributes.get()), ::CFRelease}; + + const auto frame_setter = AutoRelease{ + ::CTFramesetterCreateWithAttributedString(attr_string.get()), ::CFRelease}; + + // calculate minimal size required to render text + CFRange range; + const auto rect = ::CTFramesetterSuggestFrameSizeWithConstraints( + frame_setter.get(), CFRangeMake(0, 0), nullptr, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX), &range); + + // create a path object to render text + const auto path = AutoRelease{ + ::CGPathCreateWithRect(CGRectMake(0, 0, std::ceil(rect.width), std::ceil(rect.height)), nullptr), nullptr}; + + expect(path, "failed to create path"); + + // create a frame to render text + const auto frame = AutoRelease{ + ::CTFramesetterCreateFrame(frame_setter.get(), range, path.get(), nullptr), ::CFRelease}; + + expect(frame, "failed to create frame"); + + const auto scale = 2u; + + const auto width = static_cast(rect.width) * scale; + const auto height = static_cast(rect.height) * scale; + + // allocate enough space to store RGBA tuples for each pixel + DataBuffer pixel_data(width * height * 4); + + const auto bits_per_pixel = 8u; + const auto bytes_per_row = width * 4u; + + // create a context for rendering text + const auto context = AutoRelease{ + ::CGBitmapContextCreateWithData( + pixel_data.data(), + width, + height, + bits_per_pixel, + bytes_per_row, + colour_space, + kCGImageAlphaPremultipliedLast, + nullptr, + nullptr), + nullptr}; + + expect(context, "failed to create context"); + + // render text, pixel data will be stored in our pixel data buffer + ::CTFrameDraw(frame.get(), context.get()); + ::CGContextFlush(context.get()); + + // create a Texture from the rendered pixel data + auto *texture = Root::texture_manager().create(pixel_data, width, height, TextureUsage::IMAGE); + texture->set_flip(true); + + return texture; +} + +} diff --git a/src/graphics/mesh.cpp b/src/graphics/mesh.cpp new file mode 100644 index 00000000..9a6f3b9a --- /dev/null +++ b/src/graphics/mesh.cpp @@ -0,0 +1,14 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/mesh.h" + +namespace iris +{ + +Mesh::~Mesh() = default; + +} diff --git a/src/graphics/mesh_loader.cpp b/src/graphics/mesh_loader.cpp new file mode 100644 index 00000000..d8700977 --- /dev/null +++ b/src/graphics/mesh_loader.cpp @@ -0,0 +1,390 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/mesh_loader.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include "core/colour.h" +#include "core/error_handling.h" +#include "core/matrix4.h" +#include "core/quaternion.h" +#include "core/resource_loader.h" +#include "core/transform.h" +#include "core/vector3.h" +#include "graphics/animation.h" +#include "graphics/bone.h" +#include "graphics/skeleton.h" +#include "graphics/vertex_data.h" +#include "log/log.h" + +namespace +{ + +/** + * Helper function to convert an assimp Matrix to an engine one. + * + * @param matrix + * Assimp matrix. + * + * @returns + * Engine matrix. + */ +iris::Matrix4 convert_matrix(const ::aiMatrix4x4 &matrix) +{ + return iris::Matrix4{ + {{matrix.a1, + matrix.a2, + matrix.a3, + matrix.a4, + matrix.b1, + matrix.b2, + matrix.b3, + matrix.b4, + matrix.c1, + matrix.c2, + matrix.c3, + matrix.c4, + matrix.d1, + matrix.d2, + matrix.d3, + matrix.d4}}}; +} + +/** + * Get the animations from an assimp scene. + * + * @param scene + * Scene to get animations from. + * + * @returns + * Animations. + */ +std::vector process_animations(const ::aiScene *scene) +{ + std::vector animations{}; + + // parse each animation + for (auto i = 0u; i < scene->mNumAnimations; ++i) + { + const auto *animation = scene->mAnimations[i]; + + auto ticks_per_second = animation->mTicksPerSecond; + if (ticks_per_second == 0.0f) + { + LOG_WARN("ms", "no ticks per second - guessing"); + ticks_per_second = 33.0f; + } + + const std::chrono::milliseconds tick_time{static_cast((1.0f / ticks_per_second) * 1000u)}; + const auto duration = tick_time * static_cast(animation->mDuration); + + std::map> nodes; + + // each channel is a collection of keys + for (auto j = 0u; j < animation->mNumChannels; ++j) + { + const auto *channel = animation->mChannels[j]; + + // sanity check we have an equal number of keys for position, + // rotation and scale + iris::ensure( + (channel->mNumPositionKeys == channel->mNumRotationKeys) || + (channel->mNumRotationKeys == channel->mNumScalingKeys), + "incomplete frame data"); + + std::vector keyframes{}; + + // convert assimp keys into keyframes + for (auto k = 0u; k < channel->mNumPositionKeys; ++k) + { + const auto assimp_pos = channel->mPositionKeys[k].mValue; + const auto assimp_rot = channel->mRotationKeys[k].mValue; + const auto assimp_scale = channel->mScalingKeys[k].mValue; + const auto time = static_cast(channel->mPositionKeys[k].mTime); + + keyframes.emplace_back( + iris::Transform{ + {assimp_pos.x, assimp_pos.y, assimp_pos.z}, + {assimp_rot.x, assimp_rot.y, assimp_rot.z, assimp_rot.w}, + {assimp_scale.x, assimp_scale.y, assimp_scale.z}}, + time * tick_time); + } + + nodes[channel->mNodeName.C_Str()] = keyframes; + } + + animations.emplace_back(duration, animation->mName.C_Str(), nodes); + } + + return animations; +} + +/** + * Get the bones from an assimp mesh. + * + * Assimp has two separate concepts for animation data: + * bone - offset matrix and collection of vertices it effects + * node - hierarchical object containing transformation + * + * An assimp node may refer to a bone, or it may just represent an intermediate + * transformation between bones. We unify both of these assimp concepts into an + * engine Bone. Our Bone may or may not effect vertices and maintains the same + * hierarchy as the assimp nodes. + * + * @param mesh + * Mesh to get bones from. + * + * @param root + * Root of hierarchy for bones. + * + * @returns + * Bones. + */ +std::vector process_bones(const ::aiMesh *mesh, const ::aiNode *root) +{ + std::vector bones{}; + + std::stack> nodes; + nodes.emplace(root, std::string{}); + + // walk the node hierarchy + do + { + auto [node, parent_name] = nodes.top(); + nodes.pop(); + + const std::string name{node->mName.C_Str()}; + + // create a bone which represents the nodes transformation but effects + // no vertices + iris::Bone bone{name, parent_name, {}, {}, convert_matrix(node->mTransformation)}; + + // see if this node represents an assimp bone + for (auto i = 0u; i < mesh->mNumBones; ++i) + { + const ::aiBone *ai_bone = mesh->mBones[i]; + + if (std::string(ai_bone->mName.C_Str()) == name) + { + std::vector weights; + for (auto j = 0u; j < ai_bone->mNumWeights; ++j) + { + const auto &weight = ai_bone->mWeights[j]; + weights.emplace_back(weight.mVertexId, weight.mWeight); + } + + // replace bone with one that stores the weights as well the + // correct matrices + bone = { + name, + parent_name, + weights, + convert_matrix(ai_bone->mOffsetMatrix), + convert_matrix(node->mTransformation)}; + + break; + } + } + + bones.emplace_back(bone); + + for (auto i = 0u; i < node->mNumChildren; ++i) + { + nodes.emplace(node->mChildren[i], name); + } + } while (!nodes.empty()); + + // always return at least one default bone + if (bones.empty()) + { + bones.emplace_back("root", "", std::vector{{0u, 1.0f}}, iris::Matrix4{}, iris::Matrix4{}); + } + + return bones; +} + +/** + * Get the indices for the given mesh. + * + * @param mesh + * Mesh to get vertices for. + * + * @returns + * Indices. + */ +std::vector process_indices(const ::aiMesh *mesh) +{ + std::vector indices{}; + + for (auto i = 0u; i < mesh->mNumFaces; ++i) + { + const auto &face = mesh->mFaces[i]; + for (auto j = 0u; j < face.mNumIndices; ++j) + { + indices.emplace_back(face.mIndices[j]); + } + } + + return indices; +} + +/** + * Get the vertices for the given mesh. + * + * @param mesh + * Mesh to get vertices for. + * + * @param material + * Material for current mesh. + * + * @returns + * Vertex data. + */ +std::vector process_vertices(const ::aiMesh *mesh, const ::aiMaterial *material) +{ + std::vector vertices{}; + + for (auto i = 0u; i < mesh->mNumVertices; ++i) + { + const auto &vertex = mesh->mVertices[i]; + const auto &normal = mesh->mNormals[i]; + iris::Colour colour{1.0f, 1.0f, 1.0f}; + iris::Vector3 texture_coords{}; + iris::Vector3 tangent{}; + iris::Vector3 bitangent{}; + + // get texture coordinates if they exist + if (mesh->HasTextureCoords(0)) + { + texture_coords.x = mesh->mTextureCoords[0][i].x; + texture_coords.y = mesh->mTextureCoords[0][i].y; + + tangent.x = mesh->mTangents[i].x; + tangent.y = mesh->mTangents[i].y; + tangent.z = mesh->mTangents[i].z; + + bitangent.x = mesh->mBitangents[i].x; + bitangent.y = mesh->mBitangents[i].y; + bitangent.z = mesh->mBitangents[i].z; + } + + // only support diffuse colour + ::aiColor3D c(0.f, 0.f, 0.f); + material->Get(AI_MATKEY_COLOR_DIFFUSE, c); + colour.r = c.r; + colour.g = c.g; + colour.b = c.b; + + vertices.emplace_back( + iris::Vector3(vertex.x, vertex.y, vertex.z), + iris::Vector3(normal.x, normal.y, normal.z), + colour, + texture_coords, + tangent, + bitangent); + } + + return vertices; +} +} + +namespace iris::mesh_loader +{ + +LoadedData load(const std::string &mesh_name) +{ + const auto file_data = ResourceLoader::instance().load(mesh_name); + + // parse file using assimp + ::Assimp::Importer importer{}; + const auto *scene = importer.ReadFileFromMemory( + file_data.data(), file_data.size(), ::aiProcess_Triangulate | ::aiProcess_CalcTangentSpace); + + ensure( + (scene != nullptr) && !(scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) && (scene->mRootNode != nullptr), + std::string{"could not load mesh: "} + importer.GetErrorString()); + + const auto *root = scene->mRootNode; + aiMesh *mesh = nullptr; + + std::stack to_process; + to_process.emplace(root); + + // walk the assimp scene looking for the first mesh + // we do *not* support multiple meshes + do + { + const auto *node = to_process.top(); + to_process.pop(); + + // check if we have a node with a single mesh + if (node->mNumMeshes == 1u) + { + mesh = scene->mMeshes[node->mMeshes[0u]]; + break; + } + + // add child nodes, to keep looking for a node with a single mesh + for (auto i = 0u; i < node->mNumChildren; ++i) + { + to_process.emplace(node->mChildren[i]); + } + } while (!to_process.empty()); + + // check if we found a node with a single mesh + ensure(mesh != nullptr, "only support single mesh in file"); + + const auto *material = scene->mMaterials[mesh->mMaterialIndex]; + + LoadedData loaded_data{}; + + loaded_data.vertices = process_vertices(mesh, material); + loaded_data.indices = process_indices(mesh); + loaded_data.skeleton = {process_bones(mesh, root), process_animations(scene)}; + + // stamp in bone data into vertices + // each vertex supports four bones, so keep a track of the next + // index for each vertex + std::vector bone_indices(loaded_data.vertices.size()); + + for (const auto &bone : loaded_data.skeleton.bones()) + { + for (const auto &[id, weight] : bone.weights()) + { + if (weight == 0.0f) + { + continue; + } + + // only support four bones per vertex + if (bone_indices[id] >= 4) + { + LOG_ENGINE_WARN("mf", "too many weights {} {}", id, weight); + continue; + } + + const auto bone_index = loaded_data.skeleton.bone_index(bone.name()); + + // update vertex data with bone data + loaded_data.vertices[id].bone_ids[bone_indices[id]] = static_cast(bone_index); + loaded_data.vertices[id].bone_weights[bone_indices[id]] = weight; + + ++bone_indices[id]; + } + } + + return loaded_data; +} + +} diff --git a/src/graphics/mesh_manager.cpp b/src/graphics/mesh_manager.cpp new file mode 100644 index 00000000..6aedb9c2 --- /dev/null +++ b/src/graphics/mesh_manager.cpp @@ -0,0 +1,207 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/mesh_manager.h" + +#include +#include +#include +#include +#include +#include + +#include "core/colour.h" +#include "core/error_handling.h" +#include "core/resource_loader.h" +#include "core/transform.h" +#include "core/vector3.h" +#include "graphics/bone.h" +#include "graphics/mesh.h" +#include "graphics/mesh_loader.h" +#include "graphics/skeleton.h" +#include "graphics/texture.h" +#include "graphics/vertex_data.h" +#include "log/log.h" + +namespace iris +{ + +MeshManager::MeshManager() + : loaded_meshes_() + , loaded_skeletons_() +{ +} + +Mesh *MeshManager::sprite(const Colour &colour) +{ + // create a unique for this mesh + std::stringstream strm{}; + strm << "!sprite" << colour; + const auto id = strm.str(); + + if (loaded_meshes_.count(id) == 0u) + { + std::vector vertices{ + {{-1.0, 1.0, 0.0f}, {}, colour, {0.0f, 1.0f, 0.0f}}, + {{1.0, 1.0, 0.0f}, {}, colour, {1.0f, 1.0f, 0.0f}}, + {{1.0, -1.0, 0.0f}, {}, colour, {1.0f, 0.0f, 0.0f}}, + {{-1.0, -1.0, 0.0f}, {}, colour, {0.0f, 0.0f, 0.0f}}}; + + std::vector indices{0, 2, 1, 3, 2, 0}; + + loaded_meshes_[id] = create_mesh(vertices, indices); + } + + return loaded_meshes_[id].get(); +} + +Mesh *MeshManager::cube(const Colour &colour) +{ + // create a unique for this mesh + std::stringstream strm{}; + strm << "!cube" << colour; + const auto id = strm.str(); + + if (loaded_meshes_.count(id) == 0u) + { + std::vector vertices{ + {{1.0f, 1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, colour, {0.0f, 0.0f, 0.0f}}, + {{-1.0f, 1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, colour, {1.0f, 0.0f, 0.0f}}, + {{-1.0f, -1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, colour, {1.0f, 1.0f, 0.0f}}, + {{1.0f, -1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, colour, {0.0f, 1.0f, 0.0f}}, + {{1.0f, -1.0f, -1.0f}, {0.0f, -1.0f, 0.0f}, colour, {0.0f, 0.0f, 0.0f}}, + {{1.0f, -1.0f, 1.0f}, {0.0f, -1.0f, 0.0f}, colour, {1.0f, 0.0f, 0.0f}}, + {{-1.0f, -1.0f, 1.0f}, {0.0f, -1.0f, 0.0f}, colour, {1.0f, 1.0f, 0.0f}}, + {{-1.0f, -1.0f, -1.0f}, {0.0f, -1.0f, 0.0f}, colour, {0.0f, 1.0f, 0.0f}}, + {{-1.0f, -1.0f, -1.0f}, {-1.0f, 0.0f, 0.0f}, colour, {0.0f, 0.0f, 0.0f}}, + {{-1.0f, -1.0f, 1.0f}, {-1.0f, 0.0f, 0.0f}, colour, {1.0f, 0.0f, 0.0f}}, + {{-1.0f, 1.0f, 1.0f}, {-1.0f, 0.0f, 0.0f}, colour, {1.0f, 1.0f, 0.0f}}, + {{-1.0f, 1.0f, -1.0f}, {-1.0f, 0.0f, 0.0f}, colour, {0.0f, 1.0f, 0.0f}}, + {{-1.0f, 1.0f, -1.0f}, {0.0f, 0.0f, -1.0f}, colour, {0.0f, 0.0f, 0.0f}}, + {{1.0f, 1.0f, -1.0f}, {0.0f, 0.0f, -1.0f}, colour, {1.0f, 0.0f, 0.0f}}, + {{1.0f, -1.0f, -1.0f}, {0.0f, 0.0f, -1.0f}, colour, {1.0f, 1.0f, 0.0f}}, + {{-1.0f, -1.0f, -1.0f}, {0.0f, 0.0f, -1.0f}, colour, {0.0f, 1.0f, 0.0f}}, + {{1.0f, 1.0f, -1.0f}, {1.0f, 0.0f, 0.0f}, colour, {0.0f, 0.0f, 0.0f}}, + {{1.0f, 1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, colour, {1.0f, 0.0f, 0.0f}}, + {{1.0f, -1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}, colour, {1.0f, 1.0f, 0.0f}}, + {{1.0f, -1.0f, -1.0f}, {1.0f, 0.0f, 0.0f}, colour, {0.0f, 1.0f, 0.0f}}, + {{-1.0f, 1.0f, -1.0f}, {0.0f, 1.0f, 0.0f}, colour, {0.0f, 0.0f, 0.0f}}, + {{-1.0f, 1.0f, 1.0f}, {0.0f, 1.0f, 0.0f}, colour, {1.0f, 0.0f, 0.0f}}, + {{1.0f, 1.0f, 1.0f}, {0.0f, 1.0f, 0.0f}, colour, {1.0f, 1.0f, 0.0f}}, + {{1.0f, 1.0f, -1.0f}, {0.0f, 1.0f, 0.0f}, colour, {0.0f, 1.0f, 0.0f}}}; + + std::vector indices{0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, + 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23}; + + loaded_meshes_[id] = create_mesh(vertices, indices); + } + + return loaded_meshes_[id].get(); +} + +Mesh *MeshManager::plane(const Colour &colour, std::uint32_t divisions) +{ + expect(divisions != 0, "divisions must be >= 0"); + + // create a unique for this mesh + std::stringstream strm{}; + strm << "!plane" << colour << ":" << divisions; + const auto id = strm.str(); + + if (loaded_meshes_.count(id) == 0u) + { + std::vector vertices(static_cast(std::pow(divisions + 1u, 2u))); + + const Vector3 normal{0.0f, 0.0f, 1.0f}; + const Vector3 tangent{1.0f, 0.0f, 0.0f}; + const Vector3 bitangent{0.0f, 1.0f, 0.0f}; + + const auto width = 1.0f / static_cast(divisions); + + for (auto y = 0u; y <= divisions; ++y) + { + for (auto x = 0u; x <= divisions; ++x) + { + vertices[(y * (divisions + 1u)) + x] = { + {(x * width) - 0.5f, (y * width) - 0.5f, 0.0f}, + normal, + colour, + {(x * width), 1.0f - (y * width), 0.0f}, + tangent, + bitangent}; + } + } + + std::vector indices{}; + + for (auto y = 0u; y < divisions; ++y) + { + for (auto x = 0u; x < divisions; ++x) + { + indices.emplace_back((y * (divisions + 1u) + x)); + indices.emplace_back(((y + 1) * (divisions + 1u) + x)); + indices.emplace_back((y * (divisions + 1u) + x + 1u)); + indices.emplace_back((y * (divisions + 1u) + x + 1u)); + indices.emplace_back(((y + 1) * (divisions + 1u) + x)); + indices.emplace_back(((y + 1) * (divisions + 1u) + x + 1u)); + } + } + + loaded_meshes_[id] = create_mesh(vertices, indices); + } + + return loaded_meshes_[id].get(); +} + +Mesh *MeshManager::quad( + const Colour &colour, + const Vector3 &lower_left, + const Vector3 &lower_right, + const Vector3 &upper_left, + const Vector3 &upper_right) +{ + // create a unique for this mesh + std::stringstream strm{}; + strm << "!quad" << colour << ":" << lower_left << ":" << lower_right << ":" << upper_left << ":" << upper_right; + const auto id = strm.str(); + + if (loaded_meshes_.count(id) == 0u) + { + std::vector vertices{ + {upper_left, {}, colour, {0.0f, 1.0f, 0.0f}}, + {upper_right, {}, colour, {1.0f, 1.0f, 0.0f}}, + {lower_right, {}, colour, {1.0f, 0.0f, 0.0f}}, + {lower_left, {}, colour, {0.0f, 0.0f, 0.0f}}}; + + std::vector indices{0, 2, 1, 3, 2, 0}; + + loaded_meshes_[id] = create_mesh(vertices, indices); + } + + return loaded_meshes_[id].get(); +} + +Mesh *MeshManager::load_mesh(const std::string &mesh_file) +{ + if (loaded_meshes_.count(mesh_file) == 0u) + { + const auto &[vertices, indices, skeleton] = mesh_loader::load(mesh_file); + loaded_meshes_[mesh_file] = create_mesh(vertices, indices); + loaded_skeletons_[mesh_file] = skeleton; + } + + return loaded_meshes_[mesh_file].get(); +} + +Skeleton MeshManager::load_skeleton(const std::string &mesh_file) +{ + // load the mesh, this will also load the skeleton + load_mesh(mesh_file); + + return loaded_skeletons_[mesh_file]; +} + +} diff --git a/src/graphics/metal/CMakeLists.txt b/src/graphics/metal/CMakeLists.txt new file mode 100644 index 00000000..bc2ffe91 --- /dev/null +++ b/src/graphics/metal/CMakeLists.txt @@ -0,0 +1,25 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/graphics/metal") + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/compiler_strings.h + ${INCLUDE_ROOT}/metal_buffer.h + ${INCLUDE_ROOT}/metal_constant_buffer.h + ${INCLUDE_ROOT}/metal_default_constant_buffer_types.h + ${INCLUDE_ROOT}/metal_material.h + ${INCLUDE_ROOT}/metal_mesh.h + ${INCLUDE_ROOT}/metal_mesh_manager.h + ${INCLUDE_ROOT}/metal_render_target.h + ${INCLUDE_ROOT}/metal_renderer.h + ${INCLUDE_ROOT}/metal_texture.h + ${INCLUDE_ROOT}/metal_texture_manager.h + ${INCLUDE_ROOT}/msl_shader_compiler.h + metal_buffer.mm + metal_constant_buffer.mm + metal_material.mm + metal_mesh.mm + metal_mesh_manager.mm + metal_render_target.mm + metal_renderer.mm + metal_texture.mm + metal_texture_manager.mm + msl_shader_compiler.cpp) diff --git a/src/graphics/metal/metal_buffer.mm b/src/graphics/metal/metal_buffer.mm new file mode 100644 index 00000000..08c37293 --- /dev/null +++ b/src/graphics/metal/metal_buffer.mm @@ -0,0 +1,106 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/metal/metal_buffer.h" + +#include +#include +#include + +#import + +#include "core/macos/macos_ios_utility.h" +#include "graphics/vertex_data.h" + +namespace +{ + +/** + * Helper function to create a metal Buffer from a collection of objects. + * + * @param data + * Data to store on buffer. + * + * @returns + * Handle to metal buffer. + */ +template +id create_buffer(const std::vector &data) +{ + auto *device = iris::core::utility::metal_device(); + + // create buffer with data + return [device newBufferWithBytes:static_cast(data.data()) + length:sizeof(T) * data.size() + options:MTLResourceOptionCPUCacheModeDefault]; +} + +} + +namespace iris +{ + +MetalBuffer::MetalBuffer(const std::vector &vertex_data) + : handle_(create_buffer(vertex_data)) + , element_count_(vertex_data.size()) + , capacity_(vertex_data.size()) +{ +} + +MetalBuffer::MetalBuffer(const std::vector &index_data) + : handle_(create_buffer(index_data)) + , element_count_(index_data.size()) + , capacity_(index_data.size()) +{ +} + +id MetalBuffer::handle() const +{ + return handle_; +} + +std::size_t MetalBuffer::element_count() const +{ + return element_count_; +} + +void MetalBuffer::write(const std::vector &vertex_data) +{ + // if the new data is larger than existing buffer then allocate a new, + // larger, buffer (with the new data + // else copy new data into existing buffer + if (vertex_data.size() > capacity_) + { + handle_ = create_buffer(vertex_data); + element_count_ = vertex_data.size(); + capacity_ = element_count_; + } + else + { + std::memcpy(handle_.contents, vertex_data.data(), vertex_data.size() * sizeof(VertexData)); + element_count_ = vertex_data.size(); + } +} + +void MetalBuffer::write(const std::vector &index_data) +{ + // if the new data is larger than existing buffer then allocate a new, + // larger, buffer (with the new data + // else copy new data into existing buffer + if (index_data.size() > capacity_) + { + handle_ = create_buffer(index_data); + element_count_ = index_data.size(); + capacity_ = element_count_; + } + else + { + std::memcpy(handle_.contents, index_data.data(), index_data.size() * sizeof(std::uint32_t)); + element_count_ = index_data.size(); + } +} + +} diff --git a/src/graphics/metal/metal_constant_buffer.mm b/src/graphics/metal/metal_constant_buffer.mm new file mode 100644 index 00000000..a141a766 --- /dev/null +++ b/src/graphics/metal/metal_constant_buffer.mm @@ -0,0 +1,33 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/metal/metal_constant_buffer.h" + +#include + +#import + +#include "core/macos/macos_ios_utility.h" + +namespace iris +{ + +MetalConstantBuffer::MetalConstantBuffer(std::size_t capacity) + : buffer_(nullptr) + , capacity_(capacity) +{ + auto *device = iris::core::utility::metal_device(); + + // create buffer with data + buffer_ = [device newBufferWithLength:capacity_ options:MTLResourceOptionCPUCacheModeDefault]; +} + +id MetalConstantBuffer::handle() const +{ + return buffer_; +} + +} diff --git a/src/graphics/metal/metal_material.mm b/src/graphics/metal/metal_material.mm new file mode 100644 index 00000000..ce7b71fc --- /dev/null +++ b/src/graphics/metal/metal_material.mm @@ -0,0 +1,119 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/metal/metal_material.h" + +#include +#include +#include +#include + +#import + +#include "core/error_handling.h" +#include "core/macos/macos_ios_utility.h" +#include "graphics/lights/lighting_rig.h" +#include "graphics/mesh.h" +#include "graphics/metal/msl_shader_compiler.h" +#include "graphics/render_graph/render_graph.h" + +namespace +{ + +/** + * Helper function to load a metal shader and get a handle to a function. + * + * @param source + * Source of shader. + * + * @param function_name + * Name of function to get handle to. + * + * @returns + * Handle to function in loaded source. + */ +id load_function(const std::string &source, const std::string &function_name) +{ + auto *device = iris::core::utility::metal_device(); + + NSError *error = nullptr; + + // load source + const auto *library = [device newLibraryWithSource:iris::core::utility::string_to_nsstring(source) + options:nullptr + error:&error]; + + if (library == nullptr) + { + // an error occurred so parse error and throw + const std::string error_message{[[error localizedDescription] UTF8String]}; + throw iris::Exception("failed to load shader: " + error_message); + } + + return [library newFunctionWithName:iris::core::utility::string_to_nsstring(function_name)]; +} + +} + +namespace iris +{ + +MetalMaterial::MetalMaterial(const RenderGraph *render_graph, MTLVertexDescriptor *descriptors, LightType light_type) + : pipeline_state_() + , textures_() +{ + MSLShaderCompiler compiler{render_graph, light_type}; + + const auto vertex_program = load_function(compiler.vertex_shader(), "vertex_main"); + const auto fragment_program = load_function(compiler.fragment_shader(), "fragment_main"); + + auto *device = core::utility::metal_device(); + + // get pipeline state handle + auto *pipeline_state_descriptor = [[MTLRenderPipelineDescriptor alloc] init]; + [pipeline_state_descriptor setVertexFunction:vertex_program]; + [pipeline_state_descriptor setFragmentFunction:fragment_program]; + pipeline_state_descriptor.colorAttachments[0].pixelFormat = MTLPixelFormatRGBA16Float; + [pipeline_state_descriptor setDepthAttachmentPixelFormat:MTLPixelFormatDepth32Float]; + [pipeline_state_descriptor setVertexDescriptor:descriptors]; + + // set blend mode based on light + // ambient is always rendered first (no blending) + // directional and point are always rendered after (blending) + switch (light_type) + { + case LightType::AMBIENT: + [[[pipeline_state_descriptor colorAttachments] objectAtIndexedSubscript:0] setBlendingEnabled:false]; + break; + case LightType::DIRECTIONAL: + case LightType::POINT: + [[[pipeline_state_descriptor colorAttachments] objectAtIndexedSubscript:0] setBlendingEnabled:true]; + pipeline_state_descriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd; + pipeline_state_descriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha; + pipeline_state_descriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOne; + break; + } + + NSError *error = nullptr; + + pipeline_state_ = [device newRenderPipelineStateWithDescriptor:pipeline_state_descriptor error:&error]; + + expect(error == nullptr, "failed to create pipeline state"); + + textures_ = compiler.textures(); +} + +id MetalMaterial::pipeline_state() const +{ + return pipeline_state_; +} + +std::vector MetalMaterial::textures() const +{ + return textures_; +} + +} diff --git a/src/graphics/metal/metal_mesh.mm b/src/graphics/metal/metal_mesh.mm new file mode 100644 index 00000000..544029fd --- /dev/null +++ b/src/graphics/metal/metal_mesh.mm @@ -0,0 +1,102 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/metal/metal_mesh.h" + +#include +#include + +#import + +#include "core/exception.h" +#include "graphics/mesh.h" +#include "graphics/metal/metal_buffer.h" +#include "graphics/vertex_data.h" + +namespace +{ + +/** + * Helper function to convert engine attribute type to metal attribute type. + * + * @param type + * Engine type. + * + * @returns + * Metal type. + */ +MTLVertexFormat to_metal_format(iris::VertexAttributeType type) +{ + auto format = MTLVertexFormatInvalid; + + switch (type) + { + case iris::VertexAttributeType::FLOAT_3: format = MTLVertexFormatFloat3; break; + case iris::VertexAttributeType::FLOAT_4: format = MTLVertexFormatFloat4; break; + case iris::VertexAttributeType::UINT32_1: format = MTLVertexFormatUInt; break; + case iris::VertexAttributeType::UINT32_4: format = MTLVertexFormatUInt4; break; + default: throw iris::Exception("unknown vertex attribute type"); + } + + return format; +} + +} + +namespace iris +{ + +MetalMesh::MetalMesh( + const std::vector &vertices, + const std::vector &indices, + const VertexAttributes &attributes) + : vertex_buffer_(vertices) + , index_buffer_(indices) + , descriptors_(nullptr) +{ + descriptors_ = [[MTLVertexDescriptor alloc] init]; + + auto index = 0u; + + // convert engine attribute information to metal + for (const auto &[type, components, size, offset] : attributes) + { + descriptors_.attributes[index].format = to_metal_format(type); + descriptors_.attributes[index].offset = offset; + descriptors_.attributes[index].bufferIndex = 0u; + + ++index; + } + + descriptors_.layouts[0u].stride = attributes.size(); +} + +void MetalMesh::update_vertex_data(const std::vector &data) +{ + vertex_buffer_.write(data); +} + +void MetalMesh::update_index_data(const std::vector &data) +{ + index_buffer_.write(data); +} + +const MetalBuffer &MetalMesh::vertex_buffer() const +{ + return vertex_buffer_; +} + +const MetalBuffer &MetalMesh::index_buffer() const +{ + return index_buffer_; +} + +MTLVertexDescriptor *MetalMesh::descriptors() const +{ + return descriptors_; +} + +} diff --git a/src/graphics/metal/metal_mesh_manager.mm b/src/graphics/metal/metal_mesh_manager.mm new file mode 100644 index 00000000..b773e2d7 --- /dev/null +++ b/src/graphics/metal/metal_mesh_manager.mm @@ -0,0 +1,28 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/metal/metal_mesh_manager.h" + +#include +#include +#include + +#include "graphics/mesh.h" +#include "graphics/mesh_manager.h" +#include "graphics/metal/metal_mesh.h" +#include "graphics/vertex_data.h" + +namespace iris +{ + +std::unique_ptr MetalMeshManager::create_mesh( + const std::vector &vertices, + const std::vector &indices) const +{ + return std::make_unique(vertices, indices, DefaultVertexAttributes); +} + +} diff --git a/src/graphics/metal/metal_render_target.mm b/src/graphics/metal/metal_render_target.mm new file mode 100644 index 00000000..1e1a0fd8 --- /dev/null +++ b/src/graphics/metal/metal_render_target.mm @@ -0,0 +1,27 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/metal/metal_render_target.h" + +#include + +#import + +#include "graphics/texture.h" + +namespace iris +{ + +MetalRenderTarget::MetalRenderTarget( + std::unique_ptr colour_texture, + std::unique_ptr depth_texture) + : RenderTarget(std::move(colour_texture), std::move(depth_texture)) +{ + colour_texture_->set_flip(true); + depth_texture_->set_flip(true); +} + +} diff --git a/src/graphics/metal/metal_renderer.mm b/src/graphics/metal/metal_renderer.mm new file mode 100644 index 00000000..d7b21995 --- /dev/null +++ b/src/graphics/metal/metal_renderer.mm @@ -0,0 +1,485 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/metal/metal_renderer.h" + +#include +#include +#include +#include + +#import +#import +#import +#import + +#include "core/error_handling.h" +#include "core/macos/macos_ios_utility.h" +#include "core/matrix4.h" +#include "core/vector3.h" +#include "graphics/constant_buffer_writer.h" +#include "graphics/lights/lighting_rig.h" +#include "graphics/mesh_manager.h" +#include "graphics/metal/metal_constant_buffer.h" +#include "graphics/metal/metal_default_constant_buffer_types.h" +#include "graphics/metal/metal_material.h" +#include "graphics/metal/metal_mesh.h" +#include "graphics/metal/metal_render_target.h" +#include "graphics/metal/metal_texture.h" +#include "graphics/render_entity.h" +#include "graphics/render_graph/post_processing_node.h" +#include "graphics/render_graph/texture_node.h" +#include "graphics/render_queue_builder.h" +#include "graphics/render_target.h" +#include "graphics/window.h" +#include "log/log.h" + +namespace +{ + +/** + * Helper function to create a render encoder for render pass. + * + * @param colour + * Metal texture to write colour data to. + * + * @param depth + * Metal texture to write depth data to. + * + * @param descriptor + * Descriptor for render pass. + * + * @param depth_stencil_state + * State for depth/stencil. + * + * @param commands_buffer + * Command buffer to create encoder from. + * + * @returns + * RenderCommandEncoder object for a render pass. + */ +id create_render_encoder( + id colour, + id depth, + MTLRenderPassDescriptor *descriptor, + const id depth_stencil_state, + id command_buffer) +{ + descriptor.colorAttachments[0].texture = colour; + descriptor.depthAttachment.texture = depth; + + const auto render_encoder = [command_buffer renderCommandEncoderWithDescriptor:descriptor]; + [render_encoder setDepthStencilState:depth_stencil_state]; + [render_encoder setFrontFacingWinding:MTLWindingCounterClockwise]; + [render_encoder setCullMode:MTLCullModeBack]; + [render_encoder setTriangleFillMode:MTLTriangleFillModeFill]; + + return render_encoder; +} + +/** + * Helper function to set constant data for a render pass. + * + * @param render_encoder + * Render encoder for current pass. + * + * @param constant_buffer + * The constant buffer to write to. + * + * @param camera + * Camera for current render pass. + * + * @param light_data + * Date for the current render pass light. + * + * @param entity + * Entity being rendered. + * + * @param shadow_map + * Optional RenderTarget for shadow map. + * + * @param light + * Light for current render pass. + */ +void set_constant_data( + id render_encoder, + iris::MetalConstantBuffer &constant_buffer, + const iris::Camera *camera, + const iris::RenderEntity *entity, + const iris::RenderTarget *shadow_map, + const iris::Light *light) +{ + // this matrix is used to translate projection matrices from engine NDC to + // metal NDC + static const iris::Matrix4 metal_translate{ + {{1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f}}}; + + // set shadow map specific data, if it is present + if ((shadow_map != nullptr) && (light->type() == iris::LightType::DIRECTIONAL)) + { + const auto *directional_light = static_cast(light); + iris::DirectionalLightConstantBuffer light_consant_buffer{}; + + light_consant_buffer.proj = + iris::Matrix4::transpose(metal_translate * directional_light->shadow_camera().projection()); + light_consant_buffer.view = iris::Matrix4::transpose(directional_light->shadow_camera().view()); + + [render_encoder setVertexBytes:static_cast(&light_consant_buffer) + length:sizeof(light_consant_buffer) + atIndex:2]; + + [render_encoder setFragmentBytes:static_cast(&light_consant_buffer) + length:sizeof(light_consant_buffer) + atIndex:1]; + } + + iris::ConstantBufferWriter writer(constant_buffer); + + writer.write(iris::Matrix4::transpose(metal_translate * camera->projection())); + writer.write(iris::Matrix4::transpose(camera->view())); + writer.write(iris::Matrix4::transpose(entity->transform())); + writer.write(iris::Matrix4::transpose(entity->normal_transform())); + + // write all the bone data and ensure we advance past the end of the array + // note that the transposing of the bone matrices is done by the shader + writer.write(entity->skeleton().transforms()); + writer.advance( + sizeof(iris::DefaultConstantBuffer::bones) - entity->skeleton().transforms().size() * sizeof(iris::Matrix4)); + + writer.write(camera->position()); + writer.write(0.0f); + + writer.write(light->colour_data()); + writer.write(light->world_space_data()); + writer.write(light->attenuation_data()); + + writer.write(0.0f); + + [render_encoder setVertexBuffer:constant_buffer.handle() offset:0 atIndex:1]; + [render_encoder setFragmentBuffer:constant_buffer.handle() offset:0 atIndex:0]; +} + +/** + * Helper function to bind all textures for a material. + * + * @param render_encoder + * Encoder for current render pass. + * + * @param material + * Material to bind textures for. + * + * @param shadow_map + * Optional RenderTarget for shadow_map. + * + * @param shadow_sampler + * Sampler to use for shadow map. + */ +void bind_textures( + id render_encoder, + const iris::Material *material, + const iris::RenderTarget *shadow_map, + id shadow_sampler) +{ + // bind shadow map if present + if (shadow_map != nullptr) + { + const auto *metal_texture = static_cast(shadow_map->depth_texture()); + [render_encoder setFragmentTexture:metal_texture->handle() atIndex:0]; + [render_encoder setFragmentSamplerState:shadow_sampler atIndex:0]; + } + + // bind all textures in material + const auto textures = material->textures(); + for (auto i = 0u; i < textures.size(); ++i) + { + const auto *metal_texture = static_cast(textures[i]); + [render_encoder setVertexTexture:metal_texture->handle() atIndex:i]; + [render_encoder setFragmentTexture:metal_texture->handle() atIndex:i + 1]; + } +} + +} + +namespace iris +{ + +MetalRenderer::MetalRenderer(std::uint32_t width, std::uint32_t height) + : Renderer() + , width_(width) + , height_(height) + , command_queue_() + , descriptor_() + , drawable_() + , command_buffer_() + , depth_stencil_state_() + , render_encoder_() + , current_frame_(0u) + , frames_() + , render_encoders_() + , render_targets_() + , materials_() + , default_depth_buffer_() + , shadow_sampler_() +{ + const auto *device = iris::core::utility::metal_device(); + + command_queue_ = [device newCommandQueue]; + ensure(command_queue_ != nullptr, "could not create command queue"); + + const auto scale = 2; + + // create and setup descriptor for depth texture + auto *texture_description = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float + width:width * scale + height:height * scale + mipmapped:NO]; + [texture_description setResourceOptions:MTLResourceStorageModePrivate]; + [texture_description setUsage:MTLTextureUsageRenderTarget]; + + // create descriptor for depth checking + auto *depth_stencil_descriptor = [MTLDepthStencilDescriptor new]; + [depth_stencil_descriptor setDepthCompareFunction:MTLCompareFunctionLessEqual]; + [depth_stencil_descriptor setDepthWriteEnabled:YES]; + + // create depth state + depth_stencil_state_ = [device newDepthStencilStateWithDescriptor:depth_stencil_descriptor]; + + // create and setup a render pass descriptor + descriptor_ = [MTLRenderPassDescriptor renderPassDescriptor]; + [[[descriptor_ colorAttachments] objectAtIndexedSubscript:0] setLoadAction:MTLLoadActionClear]; + [[[descriptor_ colorAttachments] objectAtIndexedSubscript:0] + setClearColor:MTLClearColorMake(0.39f, 0.58f, 0.93f, 1.0f)]; + [[[descriptor_ colorAttachments] objectAtIndexedSubscript:0] setStoreAction:MTLStoreActionStore]; + [[[descriptor_ colorAttachments] objectAtIndexedSubscript:0] setTexture:nullptr]; + [[descriptor_ depthAttachment] setTexture:nullptr]; + [[descriptor_ depthAttachment] setClearDepth:1.0f]; + [[descriptor_ depthAttachment] setLoadAction:MTLLoadActionClear]; + [[descriptor_ depthAttachment] setStoreAction:MTLStoreActionStore]; + + // create default depth buffer + default_depth_buffer_ = std::make_unique(DataBuffer{}, width * 2u, height * 2u, TextureUsage::DEPTH); + + render_encoder_ = nullptr; + + // create sampler for shadow maps + MTLSamplerDescriptor *sampler_descriptor = [MTLSamplerDescriptor new]; + sampler_descriptor.rAddressMode = MTLSamplerAddressModeClampToBorderColor; + sampler_descriptor.sAddressMode = MTLSamplerAddressModeClampToBorderColor; + sampler_descriptor.tAddressMode = MTLSamplerAddressModeClampToBorderColor; + sampler_descriptor.borderColor = MTLSamplerBorderColorOpaqueWhite; + sampler_descriptor.minFilter = MTLSamplerMinMagFilterLinear; + sampler_descriptor.magFilter = MTLSamplerMinMagFilterLinear; + sampler_descriptor.mipFilter = MTLSamplerMipFilterNotMipmapped; + shadow_sampler_ = [device newSamplerStateWithDescriptor:sampler_descriptor]; +} + +MetalRenderer::~MetalRenderer() +{ + // we need to wait for any inflight frames to have finished being rendered + // by the GPU before we can destruct + // this line very deliberately creates an unnamed scoped_lock, this + // ensures the locks for all frames will be waited on, acquired and then + // immediately released + std::scoped_lock{frames_[0].lock, frames_[1].lock, frames_[2].lock}; +} + +void MetalRenderer::set_render_passes(const std::vector &render_passes) +{ + render_passes_ = render_passes; + + // add a post processing pass + + // find the pass which renders to the screen + auto final_pass = std::find_if( + std::begin(render_passes_), + std::end(render_passes_), + [](const RenderPass &pass) { return pass.render_target == nullptr; }); + + ensure(final_pass != std::cend(render_passes_), "no final pass"); + + // deferred creating of render target to ensure this class is full + // constructed + if (post_processing_target_ == nullptr) + { + post_processing_target_ = create_render_target(width_, height_); + post_processing_camera_ = std::make_unique(CameraType::ORTHOGRAPHIC, width_, height_); + } + + post_processing_scene_ = std::make_unique(); + + // create a full screen quad which renders the final stage with the post + // processing node + auto *rg = post_processing_scene_->create_render_graph(); + rg->set_render_node(rg->create(post_processing_target_->colour_texture())); + post_processing_scene_->create_entity( + rg, + Root::mesh_manager().sprite({}), + Transform({}, {}, {static_cast(width_), static_cast(height_), 1.0})); + + // wire up this pass + final_pass->render_target = post_processing_target_; + render_passes_.emplace_back(post_processing_scene_.get(), post_processing_camera_.get(), nullptr); + + // build the render queue from the provided passes + + RenderQueueBuilder queue_builder( + [this](RenderGraph *render_graph, RenderEntity *entity, const RenderTarget *target, LightType light_type) + { + if (materials_.count(render_graph) == 0u || materials_[render_graph].count(light_type) == 0u) + { + materials_[render_graph][light_type] = std::make_unique( + render_graph, static_cast(entity->mesh())->descriptors(), light_type); + } + + return materials_[render_graph][light_type].get(); + }, + [this](std::uint32_t width, std::uint32_t height) { return create_render_target(width, height); }); + + render_queue_ = queue_builder.build(render_passes_); + + // clear all constant data buffers + for (auto &frame : frames_) + { + frame.constant_data_buffers.clear(); + } + + // create a constant data buffer for each draw command + for (const auto &command : render_queue_) + { + if (command.type() == RenderCommandType::DRAW) + { + const auto *command_ptr = std::addressof(command); + + for (auto &frame : frames_) + { + frame.constant_data_buffers.emplace(command_ptr, sizeof(DefaultConstantBuffer)); + } + } + } +} + +RenderTarget *MetalRenderer::create_render_target(std::uint32_t width, std::uint32_t height) +{ + const auto scale = Root::window_manager().current_window()->screen_scale(); + + render_targets_.emplace_back(std::make_unique( + std::make_unique(DataBuffer{}, width * scale, height * scale, TextureUsage::RENDER_TARGET), + std::make_unique(DataBuffer{}, width * scale, height * scale, TextureUsage::DEPTH))); + + return render_targets_.back().get(); +} + +void MetalRenderer::pre_render() +{ + // acquire the lock, this will be released when the GPU has finished + // rendering the frame + frames_[current_frame_ % 3u].lock.lock(); + + const auto layer = core::utility::metal_layer(); +#if defined(IRIS_PLATFORM_MACOS) + // can only disable vsync on macos + [layer setDisplaySyncEnabled:NO]; +#endif + drawable_ = [layer nextDrawable]; + command_buffer_ = [command_queue_ commandBuffer]; + + // create render encoders fresh each frame + render_encoders_.clear(); +} + +void MetalRenderer::execute_draw(RenderCommand &command) +{ + const auto *material = static_cast(command.material()); + const auto *entity = command.render_entity(); + const auto *mesh = static_cast(entity->mesh()); + const auto *camera = command.render_pass()->camera; + const auto *target = command.render_pass()->render_target; + + // create a render encoder based on the render command target + if (render_encoders_.count(target) == 0u) + { + id encoder; + if (target == nullptr) + { + // no target means render to the window frame buffer + encoder = create_render_encoder( + drawable_.texture, default_depth_buffer_->handle(), descriptor_, depth_stencil_state_, command_buffer_); + } + else + { + encoder = create_render_encoder( + static_cast(target->colour_texture())->handle(), + static_cast(target->depth_texture())->handle(), + descriptor_, + depth_stencil_state_, + command_buffer_); + } + + render_encoders_[target] = encoder; + } + + render_encoder_ = render_encoders_[target]; + + const auto frame = current_frame_ % 3u; + + set_constant_data( + render_encoder_, + frames_[frame].constant_data_buffers.at(std::addressof(command)), + camera, + entity, + command.shadow_map(), + command.light()); + bind_textures(render_encoder_, material, command.shadow_map(), shadow_sampler_); + + const auto &vertex_buffer = mesh->vertex_buffer(); + const auto &index_buffer = mesh->index_buffer(); + + // encode render commands + [render_encoder_ setRenderPipelineState:material->pipeline_state()]; + [render_encoder_ setVertexBuffer:vertex_buffer.handle() offset:0 atIndex:0]; + [render_encoder_ setCullMode:MTLCullModeNone]; + + const auto type = + entity->primitive_type() == iris::PrimitiveType::TRIANGLES ? MTLPrimitiveTypeTriangle : MTLPrimitiveTypeLine; + + // draw command + [render_encoder_ drawIndexedPrimitives:type + indexCount:index_buffer.element_count() + indexType:MTLIndexTypeUInt32 + indexBuffer:index_buffer.handle() + indexBufferOffset:0]; +} + +void MetalRenderer::execute_pass_end(RenderCommand &) +{ + // end encoding for pass + [render_encoder_ endEncoding]; +} + +void MetalRenderer::execute_present(RenderCommand &) +{ + [command_buffer_ presentDrawable:drawable_]; + + // store local copy of frame so it can be correctly accessed via the + // completion handler + const auto frame = current_frame_ % 3u; + + // set completion handler for command buffer, when the GPU is finished + // rendering it will fire the handler, which will unlock the lock, allowing + // the frame to be rendered to again + [command_buffer_ addCompletedHandler:^(id commandBuffer) { + frames_[frame].lock.unlock(); + }]; + + [command_buffer_ commit]; +} + +void MetalRenderer::post_render() +{ + ++current_frame_; +} + +} diff --git a/src/graphics/metal/metal_texture.mm b/src/graphics/metal/metal_texture.mm new file mode 100644 index 00000000..00142fd1 --- /dev/null +++ b/src/graphics/metal/metal_texture.mm @@ -0,0 +1,208 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/metal/metal_texture.h" + +#include +#include +#include +#include +#include +#include + +#import + +#include "core/data_buffer.h" +#include "core/exception.h" +#include "core/macos/macos_ios_utility.h" +#include "core/resource_loader.h" +#include "core/vector3.h" +#include "graphics/texture_usage.h" +#include "log/log.h" + +namespace +{ + +/** + * Helper function to create a metal texture descriptor suitable for the texture + * usage. + * + * @param width + * Width of texture. + * + * @param height + * Height of texture. + * + * @returns + * MTLTextureDescriptor for texture. + */ +MTLTextureDescriptor *image_texture_descriptor(std::uint32_t width, std::uint32_t height) +{ + auto *texture_descriptor = [MTLTextureDescriptor new]; + texture_descriptor.textureType = MTLTextureType2D; + texture_descriptor.width = width; + texture_descriptor.height = height; + texture_descriptor.pixelFormat = MTLPixelFormatRGBA8Unorm_sRGB; + texture_descriptor.usage = MTLTextureUsageShaderRead; + + return texture_descriptor; +} + +/** + * Helper function to create a metal texture descriptor suitable for the data + * usage. + * + * @param width + * Width of texture. + * + * @param height + * Height of texture. + * + * @returns + * MTLTextureDescriptor for texture. + */ +MTLTextureDescriptor *data_texture_descriptor(std::uint32_t width, std::uint32_t height) +{ + auto *texture_descriptor = [MTLTextureDescriptor new]; + texture_descriptor.textureType = MTLTextureType2D; + texture_descriptor.width = width; + texture_descriptor.height = height; + texture_descriptor.pixelFormat = MTLPixelFormatRGBA8Unorm; + texture_descriptor.usage = MTLTextureUsageShaderRead; + + return texture_descriptor; +} + +/** + * Helper function to create a metal texture descriptor suitable for the render + * target usage. + * + * @param width + * Width of texture. + * + * @param height + * Height of texture. + * + * @returns + * MTLTextureDescriptor for texture. + */ +MTLTextureDescriptor *render_target_texture_descriptor(std::uint32_t width, std::uint32_t height) +{ + auto *texture_descriptor = [MTLTextureDescriptor new]; + texture_descriptor.textureType = MTLTextureType2D; + texture_descriptor.width = width; + texture_descriptor.height = height; + texture_descriptor.pixelFormat = MTLPixelFormatRGBA16Float; + texture_descriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; + + return texture_descriptor; +} + +/** + * Helper function to create a metal texture descriptor suitable for the depth + * usage. + * + * @param width + * Width of texture. + * + * @param height + * Height of texture. + * + * @returns + * MTLTextureDescriptor for texture. + */ +MTLTextureDescriptor *depth_texture_descriptor(std::uint32_t width, std::uint32_t height) +{ + auto *texture_descriptor = [MTLTextureDescriptor new]; + texture_descriptor.textureType = MTLTextureType2D; + texture_descriptor.width = width; + texture_descriptor.height = height; + texture_descriptor.pixelFormat = MTLPixelFormatDepth32Float; + texture_descriptor.resourceOptions = MTLResourceStorageModePrivate; + texture_descriptor.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead; + + return texture_descriptor; +} + +/** + * Helper function to create a metal Texture from pixel data. + * + * @param data + * Raw data of image. + * + * @param width + * Width of image. + * + * @param height + * Height of image. + * + * @param usage + * Texture usage. + * + * @returns + * Handle to texture. + */ +id create_texture( + iris::DataBuffer data, + std::uint32_t width, + std::uint32_t height, + iris::TextureUsage usage) +{ + auto *data_ptr = data.data(); + + iris::DataBuffer padded{}; + + MTLTextureDescriptor *texture_descriptor = nullptr; + + switch (usage) + { + case iris::TextureUsage::IMAGE: texture_descriptor = image_texture_descriptor(width, height); break; + case iris::TextureUsage::DATA: texture_descriptor = data_texture_descriptor(width, height); break; + case iris::TextureUsage::RENDER_TARGET: + texture_descriptor = render_target_texture_descriptor(width, height); + break; + case iris::TextureUsage::DEPTH: texture_descriptor = depth_texture_descriptor(width, height); break; + default: throw iris::Exception("unknown texture usage"); + } + + auto *device = iris::core::utility::metal_device(); + + // create new texture + auto texture = [device newTextureWithDescriptor:texture_descriptor]; + + // set data if it was supplied + if (!data.empty()) + { + auto region = MTLRegionMake2D(0, 0, width, height); + const auto bytes_per_row = width * 4u; + + // set image data for texture + [texture replaceRegion:region mipmapLevel:0 withBytes:data_ptr bytesPerRow:bytes_per_row]; + } + + return texture; +} + +} + +namespace iris +{ + +MetalTexture::MetalTexture(const DataBuffer &data, std::uint32_t width, std::uint32_t height, TextureUsage usage) + : Texture(data, width, height, usage) + , texture_() +{ + texture_ = create_texture(data, width, height, usage); + + LOG_ENGINE_INFO("texture", "loaded from data"); +} + +id MetalTexture::handle() const +{ + return texture_; +} + +} diff --git a/src/graphics/metal/metal_texture_manager.mm b/src/graphics/metal/metal_texture_manager.mm new file mode 100644 index 00000000..e16f5230 --- /dev/null +++ b/src/graphics/metal/metal_texture_manager.mm @@ -0,0 +1,29 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/metal/metal_texture_manager.h" + +#include +#include + +#include "core/data_buffer.h" +#include "graphics/metal/metal_texture.h" +#include "graphics/texture.h" +#include "graphics/texture_manager.h" +#include "graphics/texture_usage.h" + +namespace iris +{ +std::unique_ptr MetalTextureManager::do_create( + const DataBuffer &data, + std::uint32_t width, + std::uint32_t height, + TextureUsage usage) +{ + return std::make_unique(data, width, height, usage); +} + +} diff --git a/src/graphics/metal/msl_shader_compiler.cpp b/src/graphics/metal/msl_shader_compiler.cpp new file mode 100644 index 00000000..c2270e24 --- /dev/null +++ b/src/graphics/metal/msl_shader_compiler.cpp @@ -0,0 +1,527 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/metal/msl_shader_compiler.h" + +#include +#include + +#include "core/colour.h" +#include "core/exception.h" +#include "core/vector3.h" +#include "graphics/lights/lighting_rig.h" +#include "graphics/metal/compiler_strings.h" +#include "graphics/render_graph/arithmetic_node.h" +#include "graphics/render_graph/blur_node.h" +#include "graphics/render_graph/colour_node.h" +#include "graphics/render_graph/combine_node.h" +#include "graphics/render_graph/component_node.h" +#include "graphics/render_graph/composite_node.h" +#include "graphics/render_graph/conditional_node.h" +#include "graphics/render_graph/invert_node.h" +#include "graphics/render_graph/post_processing_node.h" +#include "graphics/render_graph/render_node.h" +#include "graphics/render_graph/sin_node.h" +#include "graphics/render_graph/texture_node.h" +#include "graphics/render_graph/value_node.h" +#include "graphics/render_graph/vertex_position_node.h" +#include "graphics/texture.h" +#include "graphics/vertex_attributes.h" + +namespace +{ + +/** + * Helper function to create a variable name for a texture. Always returns the + * same name for the same texture. + * + * @param texture + * Texture to generate name for. + * + * @param textures + * Collection of textures, texture will be inserted if it does not exist. + * + * @returns + * Unique variable name for the texture. + */ +std::string texture_name(iris::Texture *texture, std::vector &textures) +{ + std::size_t id = 0u; + + const auto find = std::find(std::cbegin(textures), std::cend(textures), texture); + + if (find != std::cend(textures)) + { + id = std::distance(std::cbegin(textures), find); + } + else + { + id = textures.size(); + textures.emplace_back(texture); + } + + return "tex" + std::to_string(id); +} + +/** + * Visit a node if it exists or write out a default value to a stream. + * + * This helper function is a little bit brittle as it assumes the visitor will + * write to the same stream as the one supplied. + * + * @param strm + * Stream to write to. + * + * @param node + * Node to visit, if nullptr then default value will be written to stream. + * + * @param visitor + * Visitor to visit node if not null. + * + * @param default_value + * Value to write to stream if node not null. + * + * @param add_semi_colon + * If true write semi colon after visit/value, else do nothing. + */ +void visit_or_default( + std::stringstream &strm, + const iris::Node *node, + iris::MSLShaderCompiler *visitor, + const std::string &default_value, + bool add_semi_colon = true) +{ + if (node == nullptr) + { + strm << default_value; + } + else + { + node->accept(*visitor); + } + + if (add_semi_colon) + { + strm << ";\n"; + } +} + +/** + * Write shader code for generating fragment colour. + * + * @param strm + * Stream to write to. + * + * @param colour + * Node for colour (maybe nullptr). + * + * @param visitor + * Visitor for node. + */ +void build_fragment_colour(std::stringstream &strm, const iris::Node *colour, iris::MSLShaderCompiler *visitor) +{ + strm << "float4 fragment_colour = "; + visit_or_default(strm, colour, visitor, "in.color"); +} + +/** + * Write shader code for generating fragment normal. + * + * @param strm + * Stream to write to. + * + * @param normal + * Node for normal (maybe nullptr). + * + * @param visitor + * Visitor for node. + */ +void build_normal(std::stringstream &strm, const iris::Node *normal, iris::MSLShaderCompiler *visitor) +{ + strm << "float3 n = "; + if (normal == nullptr) + { + strm << "normalize(in.normal.xyz);\n"; + } + else + { + strm << "float3("; + normal->accept(*visitor); + strm << ");\n"; + strm << "n = normalize(n * 2.0 - 1.0);\n"; + } +} + +} + +namespace iris +{ + +MSLShaderCompiler::MSLShaderCompiler(const RenderGraph *render_graph, LightType light_type) + : vertex_stream_() + , fragment_stream_() + , current_stream_(nullptr) + , vertex_functions_() + , fragment_functions_() + , current_functions_(nullptr) + , textures_() + , light_type_(light_type) +{ + render_graph->render_node()->accept(*this); +} + +void MSLShaderCompiler::visit(const RenderNode &node) +{ + current_stream_ = &vertex_stream_; + current_functions_ = &vertex_functions_; + + current_functions_->emplace(bone_transform_function); + current_functions_->emplace(tbn_function); + + // build vertex shader + + vertex_stream_ << vertex_begin; + + if (light_type_ == LightType::DIRECTIONAL) + { + *current_stream_ << R"( + out.frag_pos_light_space = light_uniform->proj * light_uniform->view * out.frag_position; +)"; + } + + *current_stream_ << "return out;"; + *current_stream_ << "}"; + + current_stream_ = &fragment_stream_; + current_functions_ = &fragment_functions_; + + current_functions_->emplace(shadow_function); + + // build fragment shader + + *current_stream_ << "float2 uv = in.tex.xy;\n"; + + build_fragment_colour(fragment_stream_, node.colour_input(), this); + build_normal(fragment_stream_, node.normal_input(), this); + + // depending on the light type depends on how we interpret the light + // uniform and how we calculate lighting + switch (light_type_) + { + case LightType::AMBIENT: *current_stream_ << "return uniform->light_colour * fragment_colour;\n"; break; + case LightType::DIRECTIONAL: + *current_stream_ << "float3 light_dir = "; + *current_stream_ + << (node.normal_input() == nullptr ? "normalize(-uniform->light_position.xyz);\n" + : "normalize(-in.tangent_light_pos.xyz);\n"); + + *current_stream_ << "float shadow = 0.0;\n"; + *current_stream_ << + R"(shadow = calculate_shadow(n, in.frag_pos_light_space, light_dir, shadow_map, shadow_sampler); + )"; + + *current_stream_ << R"( + float diff = (1.0 - shadow) * max(dot(n, light_dir), 0.0); + float3 diffuse = float3(diff); + + return float4(diffuse * fragment_colour.xyz, 1.0); + )"; + break; + case LightType::POINT: + *current_stream_ << "float3 light_dir = "; + *current_stream_ + << (node.normal_input() == nullptr ? "normalize(uniform->light_position.xyz - " + "in.frag_position.xyz);\n" + : "normalize(in.tangent_light_pos.xyz - " + "in.tangent_frag_pos.xyz);\n"); + *current_stream_ << R"( + float distance = length(uniform->light_position.xyz - in.frag_position.xyz); + float constant_term = uniform->light_attenuation[0]; + float linear = uniform->light_attenuation[1]; + float quadratic = uniform->light_attenuation[2]; + float attenuation = 1.0 / (constant_term + linear * distance + quadratic * (distance * distance)); + float3 att = float3(attenuation, attenuation, attenuation); + + float diff = max(dot(n, light_dir), 0.0); + float3 diffuse = float3(diff, diff, diff); + + return float4(diffuse * uniform->light_colour.xyz * fragment_colour.xyz * att, 1.0); + )"; + break; + } + + *current_stream_ << "}"; +} + +void MSLShaderCompiler::visit(const PostProcessingNode &node) +{ + current_stream_ = &vertex_stream_; + current_functions_ = &vertex_functions_; + + current_functions_->emplace(bone_transform_function); + current_functions_->emplace(tbn_function); + + // build vertex shader + + vertex_stream_ << vertex_begin; + *current_stream_ << "return out;"; + *current_stream_ << "}"; + + current_stream_ = &fragment_stream_; + current_functions_ = &fragment_functions_; + + // build fragment shader + + *current_stream_ << "float2 uv = in.tex.xy;\n"; + + build_fragment_colour(fragment_stream_, node.colour_input(), this); + + *current_stream_ << R"( + float3 mapped = fragment_colour.rgb / (fragment_colour.rgb + float3(1.0, 1.0, 1.0)); + mapped = pow(mapped, float3(1.0 / 2.2)); + + return float4(mapped, 1.0); + })"; +} + +void MSLShaderCompiler::visit(const ColourNode &node) +{ + const auto colour = node.colour(); + *current_stream_ << "float4(" << colour.r << ", " << colour.g << ", " << colour.b << ", " << colour.a << ")"; +} + +void MSLShaderCompiler::visit(const TextureNode &node) +{ + current_functions_->emplace(R"( +float4 sample_texture(texture2d texture, float2 coord) +{ + constexpr sampler s(coord::normalized, address::repeat, filter::linear); + return texture.sample(s, coord); +} +)"); + + *current_stream_ << "sample_texture(" << texture_name(node.texture(), textures_); + + if (node.texture()->flip()) + { + *current_stream_ << ", float2(uv.x, -uv.y))"; + } + else + { + *current_stream_ << ", uv)"; + } +} + +void MSLShaderCompiler::visit(const InvertNode &node) +{ + current_functions_->emplace(invert_function); + + *current_stream_ << "invert("; + node.input_node()->accept(*this); + *current_stream_ << ")"; +} + +void MSLShaderCompiler::visit(const BlurNode &node) +{ + current_functions_->emplace(blur_function); + + *current_stream_ << "blur(" << texture_name(node.input_node()->texture(), textures_); + + if (node.input_node()->texture()->flip()) + { + *current_stream_ << ", float2(uv.x, -uv.y))"; + } + else + { + *current_stream_ << ", uv)"; + } +} + +void MSLShaderCompiler::visit(const CompositeNode &node) +{ + current_functions_->emplace(composite_function); + + *current_stream_ << "composite("; + node.colour1()->accept(*this); + *current_stream_ << ", "; + node.colour2()->accept(*this); + *current_stream_ << ", "; + node.depth1()->accept(*this); + *current_stream_ << ", "; + node.depth2()->accept(*this); + *current_stream_ << ", uv)"; +} + +void MSLShaderCompiler::visit(const VertexPositionNode &node) +{ + *current_stream_ << "vertices[vid].position"; +} + +void MSLShaderCompiler::visit(const ValueNode &node) +{ + *current_stream_ << std::to_string(node.value()); +} + +void MSLShaderCompiler::visit(const ValueNode &node) +{ + *current_stream_ << "float3(" << node.value().x << ", " << node.value().y << ", " << node.value().z << ")"; +} + +void MSLShaderCompiler::visit(const ValueNode &node) +{ + *current_stream_ << "float4(" << node.value().g << ", " << node.value().g << ", " << node.value().b << ", " + << node.value().a << ")"; +} + +void MSLShaderCompiler::visit(const ArithmeticNode &node) +{ + *current_stream_ << "("; + if (node.arithmetic_operator() == ArithmeticOperator::DOT) + { + *current_stream_ << "dot("; + node.value1()->accept(*this); + *current_stream_ << ", "; + node.value2()->accept(*this); + *current_stream_ << ")"; + } + else + { + node.value1()->accept(*this); + switch (node.arithmetic_operator()) + { + case ArithmeticOperator::ADD: *current_stream_ << " + "; break; + case ArithmeticOperator::SUBTRACT: *current_stream_ << " - "; break; + case ArithmeticOperator::MULTIPLY: *current_stream_ << " * "; break; + case ArithmeticOperator::DIVIDE: *current_stream_ << " / "; break; + default: throw Exception("unknown arithmetic operator"); + } + node.value2()->accept(*this); + } + + *current_stream_ << ")"; +} + +void MSLShaderCompiler::visit(const ConditionalNode &node) +{ + *current_stream_ << "("; + node.input_value1()->accept(*this); + + switch (node.conditional_operator()) + { + case ConditionalOperator::GREATER: *current_stream_ << " > "; break; + } + + node.input_value2()->accept(*this); + + *current_stream_ << " ? "; + node.output_value1()->accept(*this); + *current_stream_ << " : "; + node.output_value2()->accept(*this); + *current_stream_ << ")"; +} + +void MSLShaderCompiler::visit(const ComponentNode &node) +{ + node.input_node()->accept(*this); + *current_stream_ << "." << node.component(); +} + +void MSLShaderCompiler::visit(const CombineNode &node) +{ + *current_stream_ << "float4("; + node.value1()->accept(*this); + *current_stream_ << ", "; + node.value2()->accept(*this); + *current_stream_ << ", "; + node.value3()->accept(*this); + *current_stream_ << ", "; + node.value4()->accept(*this); + *current_stream_ << ")"; +} + +void MSLShaderCompiler::visit(const SinNode &node) +{ + *current_stream_ << "sin("; + node.input_node()->accept(*this); + *current_stream_ << ")"; +} + +std::string MSLShaderCompiler::vertex_shader() const +{ + std::stringstream stream{}; + + stream << preamble << '\n'; + stream << vertex_in << '\n'; + stream << vertex_out << '\n'; + stream << default_uniform << '\n'; + stream << directional_light_uniform << '\n'; + stream << point_light_uniform << '\n'; + + for (const auto &function : vertex_functions_) + { + stream << function << '\n'; + } + + stream << R"( + vertex VertexOut vertex_main( + device VertexIn *vertices [[buffer(0)]], + constant DefaultUniform *uniform [[buffer(1)]], + constant DirectionalLightUniform *light_uniform [[buffer(2)]], + uint vid [[vertex_id]])"; + for (auto i = 0u; i < textures_.size(); ++i) + { + stream << " ,texture2d tex" << i << " [[texture(" << i << ")]]" << '\n'; + } + stream << R"() {)"; + + stream << vertex_stream_.str() << '\n'; + + return stream.str(); +} + +std::string MSLShaderCompiler::fragment_shader() const +{ + std::stringstream stream{}; + + stream << preamble << '\n'; + stream << vertex_in << '\n'; + stream << vertex_out << '\n'; + stream << default_uniform << '\n'; + stream << directional_light_uniform << '\n'; + stream << point_light_uniform << '\n'; + + for (const auto &function : fragment_functions_) + { + stream << function << '\n'; + } + + stream << R"( +fragment float4 fragment_main( + VertexOut in [[stage_in]], + constant DefaultUniform *uniform [[buffer(0)]], + constant DirectionalLightUniform *light_uniform [[buffer(1)]], + sampler shadow_sampler [[sampler(0)]], + texture2d shadow_map [[texture(0)]])"; + + for (auto i = 0u; i < textures_.size(); ++i) + { + stream << " ,texture2d tex" << i << " [[texture(" << i + 1 << ")]]" << '\n'; + } + stream << R"( + ) +{ +)"; + + stream << fragment_stream_.str() << '\n'; + + return stream.str(); +} + +std::vector MSLShaderCompiler::textures() const +{ + return textures_; +} +} diff --git a/src/graphics/opengl/CMakeLists.txt b/src/graphics/opengl/CMakeLists.txt new file mode 100644 index 00000000..7a79f867 --- /dev/null +++ b/src/graphics/opengl/CMakeLists.txt @@ -0,0 +1,33 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/graphics/opengl") + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/compiler_strings.h + ${INCLUDE_ROOT}/default_uniforms.h + ${INCLUDE_ROOT}/glsl_shader_compiler.h + ${INCLUDE_ROOT}/opengl.h + ${INCLUDE_ROOT}/opengl_buffer.h + ${INCLUDE_ROOT}/opengl_material.h + ${INCLUDE_ROOT}/opengl_mesh.h + ${INCLUDE_ROOT}/opengl_mesh_manager.h + ${INCLUDE_ROOT}/opengl_render_target.h + ${INCLUDE_ROOT}/opengl_renderer.h + ${INCLUDE_ROOT}/opengl_shader.h + ${INCLUDE_ROOT}/opengl_texture.h + ${INCLUDE_ROOT}/opengl_texture_manager.h + ${INCLUDE_ROOT}/opengl_uniform.h + glsl_shader_compiler.cpp + opengl.cpp + opengl_buffer.cpp + opengl_material.cpp + opengl_mesh.cpp + opengl_mesh_manager.cpp + opengl_render_target.cpp + opengl_renderer.cpp + opengl_shader.cpp + opengl_texture.cpp + opengl_texture_manager.cpp + opengl_uniform.cpp) + +if(IRIS_PLATFORM MATCHES "WIN32") + target_sources(iris PRIVATE ${INCLUDE_ROOT}/opengl_windows.h) +endif() diff --git a/src/graphics/opengl/glsl_shader_compiler.cpp b/src/graphics/opengl/glsl_shader_compiler.cpp new file mode 100644 index 00000000..af1564dd --- /dev/null +++ b/src/graphics/opengl/glsl_shader_compiler.cpp @@ -0,0 +1,522 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/opengl/glsl_shader_compiler.h" + +#include +#include + +#include "core/colour.h" +#include "core/exception.h" +#include "core/vector3.h" +#include "graphics/lights/lighting_rig.h" +#include "graphics/opengl/compiler_strings.h" +#include "graphics/opengl/opengl_texture.h" +#include "graphics/render_graph/arithmetic_node.h" +#include "graphics/render_graph/blur_node.h" +#include "graphics/render_graph/colour_node.h" +#include "graphics/render_graph/combine_node.h" +#include "graphics/render_graph/component_node.h" +#include "graphics/render_graph/composite_node.h" +#include "graphics/render_graph/conditional_node.h" +#include "graphics/render_graph/invert_node.h" +#include "graphics/render_graph/post_processing_node.h" +#include "graphics/render_graph/render_node.h" +#include "graphics/render_graph/sin_node.h" +#include "graphics/render_graph/texture_node.h" +#include "graphics/render_graph/value_node.h" +#include "graphics/render_graph/vertex_position_node.h" +#include "graphics/texture.h" + +namespace +{ + +/** + * Helper function to create a variable name for a texture. Always returns the + * same name for the same texture. + * + * @param texture + * Texture to generate name for. + * + * @param textures + * Collection of textures, texture will be inserted if it does not exist. + * + * @returns + * Unique variable name for the texture. + */ +std::string texture_name(iris::Texture *texture, std::vector &textures) +{ + std::size_t id = 0u; + + // id will be index into the textures collection + + const auto find = std::find(std::cbegin(textures), std::cend(textures), texture); + + if (find != std::cend(textures)) + { + id = std::distance(std::cbegin(textures), find); + } + else + { + id = textures.size(); + textures.emplace_back(texture); + } + + return "texture" + std::to_string(id); +} + +/** + * Visit a node if it exists or write out a default value to a stream. + * + * This helper function is a little bit brittle as it assumes the visitor will + * write to the same stream as the one supplied. + * + * @param strm + * Stream to write to. + * + * @param node + * Node to visit, if nullptr then default value will be written to stream. + * + * @param visitor + * Visitor to visit node if not null. + * + * @param default_value + * Value to write to stream if node not null. + * + * @param add_semi_colon + * If true write semi colon after visit/value, else do nothing. + */ +void visit_or_default( + std::stringstream &strm, + const iris::Node *node, + iris::GLSLShaderCompiler *visitor, + const std::string &default_value, + bool add_semi_colon = true) +{ + if (node == nullptr) + { + strm << default_value; + } + else + { + node->accept(*visitor); + } + + if (add_semi_colon) + { + strm << ";\n"; + } +} + +/** + * Write shader code for generating tangent values to a stream. + * + * @param strm + * Stream to write to. + * + * @param light_type + * Type if light to render with. + */ +void build_tangent_values(std::stringstream &strm, iris::LightType light_type) +{ + if (light_type == iris::LightType::DIRECTIONAL) + { + strm << "frag_pos_light_space = light_projection * light_view * " + "frag_pos;\n"; + } + + strm << "tangent_light_pos = tbn * light_position.xyz;\n"; + strm << "tangent_view_pos = tbn * camera_.xyz;\n"; + strm << "tangent_frag_pos = tbn * frag_pos.xyz;\n"; +} + +/** + * Write shader code for generating fragment colour. + * + * @param strm + * Stream to write to. + * + * @param colour + * Node for colour (maybe nullptr). + * + * @param visitor + * Visitor for node. + */ +void build_fragment_colour(std::stringstream &strm, const iris::Node *colour, iris::GLSLShaderCompiler *visitor) +{ + strm << "vec4 fragment_colour = "; + visit_or_default(strm, colour, visitor, "col"); +} + +/** + * Write shader code for generating fragment normal. + * + * @param strm + * Stream to write to. + * + * @param normal + * Node for normal (maybe nullptr). + * + * @param visitor + * Visitor for node. + */ +void build_normal(std::stringstream &strm, const iris::Node *normal, iris::GLSLShaderCompiler *visitor) +{ + strm << "vec3 n = "; + + if (normal == nullptr) + { + strm << "normalize(norm.xyz);\n"; + } + else + { + strm << "vec3("; + normal->accept(*visitor); + strm << ");\n"; + strm << "n = normalize(n * 2.0 - 1.0);\n"; + } +} + +} + +namespace iris +{ +GLSLShaderCompiler::GLSLShaderCompiler(const RenderGraph *render_graph, LightType light_type) + : vertex_stream_() + , fragment_stream_() + , current_stream_(nullptr) + , vertex_functions_() + , fragment_functions_() + , current_functions_(nullptr) + , textures_() + , light_type_(light_type) +{ + render_graph->render_node()->accept(*this); +} + +void GLSLShaderCompiler::visit(const RenderNode &node) +{ + current_stream_ = &vertex_stream_; + current_functions_ = &vertex_functions_; + + current_functions_->emplace(bone_transform_function); + current_functions_->emplace(tbn_function); + + // build vertex shader + + vertex_stream_ << " void main()\n{\n"; + vertex_stream_ << vertex_begin; + build_tangent_values(vertex_stream_, light_type_); + vertex_stream_ << "}"; + + current_stream_ = &fragment_stream_; + current_functions_ = &fragment_functions_; + + current_functions_->emplace(shadow_function); + + // build fragment shader + + fragment_stream_ << "void main()\n{\n"; + + if (!node.is_depth_only()) + { + // build basic values + build_fragment_colour(*current_stream_, node.colour_input(), this); + build_normal(*current_stream_, node.normal_input(), this); + + // depending on the light type depends on how we interpret the light + // uniform and how we calculate lighting + switch (light_type_) + { + case LightType::AMBIENT: + *current_stream_ << R"( + outColour = light_colour * fragment_colour;)"; + break; + case LightType::DIRECTIONAL: + *current_stream_ << "vec3 light_dir = "; + *current_stream_ + << (node.normal_input() == nullptr ? "normalize(-light_position.xyz);\n" + : "normalize(-tangent_light_pos.xyz);\n"); + + *current_stream_ << "float shadow = 0.0;\n"; + *current_stream_ << + R"(shadow = calculate_shadow(n, frag_pos_light_space, light_position.xyz, g_shadow_map); + )"; + + *current_stream_ << R"( + float diff = (1.0 - shadow) * max(dot(n, light_dir), 0.0); + vec3 diffuse = vec3(diff); + + outColour = vec4(diffuse * fragment_colour.xyz, 1.0); + )"; + break; + case LightType::POINT: + *current_stream_ << "vec3 light_dir = "; + *current_stream_ + << (node.normal_input() == nullptr ? "normalize(light_position.xyz - frag_pos.xyz);\n" + : "normalize(tangent_light_pos.xyz - " + "tangent_frag_pos.xyz);\n"); + *current_stream_ << R"( + float distance = length(light_position.xyz - frag_pos.xyz); + float constant = light_attenuation[0]; + float linear = light_attenuation[1]; + float quadratic = light_attenuation[2]; + float attenuation = 1.0 / (constant + linear * distance + quadratic * (distance * distance)); + + float diff = max(dot(n, light_dir), 0.0); + vec3 diffuse = vec3(diff); + + outColour = vec4(diffuse * light_colour.xyz * fragment_colour.xyz * vec3(attenuation), 1.0); + )"; + break; + } + + *current_stream_ << R"( + if (fragment_colour.a < 0.01) + { + discard; + } +)"; + } + fragment_stream_ << "}"; +} + +void GLSLShaderCompiler::visit(const PostProcessingNode &node) +{ + current_stream_ = &vertex_stream_; + current_functions_ = &vertex_functions_; + + current_functions_->emplace(bone_transform_function); + current_functions_->emplace(tbn_function); + + // build vertex shader + + vertex_stream_ << " void main()\n{\n"; + vertex_stream_ << vertex_begin; + vertex_stream_ << "}"; + + current_stream_ = &fragment_stream_; + current_functions_ = &fragment_functions_; + + current_functions_->emplace(shadow_function); + + // build fragment shader + + fragment_stream_ << "void main()\n{\n"; + + // build basic values + build_fragment_colour(*current_stream_, node.colour_input(), this); + + *current_stream_ << R"( + vec3 mapped = fragment_colour.rgb / (fragment_colour.rgb + vec3(1.0)); + mapped = pow(mapped, vec3(1.0 / 2.2)); + + outColour = vec4(mapped, 1.0); + })"; +} + +void GLSLShaderCompiler::visit(const ColourNode &node) +{ + const auto colour = node.colour(); + *current_stream_ << "vec4(" << colour.r << ", " << colour.g << ", " << colour.b << ", " << colour.a << ")"; +} + +void GLSLShaderCompiler::visit(const TextureNode &node) +{ + *current_stream_ << "texture(" << texture_name(node.texture(), textures_); + + if (node.texture()->flip()) + { + *current_stream_ << ", vec2(tex_coord.s, 1.0 - tex_coord.t))"; + } + else + { + *current_stream_ << ", tex_coord)"; + } +} + +void GLSLShaderCompiler::visit(const InvertNode &node) +{ + current_functions_->emplace(invert_function); + + *current_stream_ << "invert("; + node.input_node()->accept(*this); + *current_stream_ << ")"; +} + +void GLSLShaderCompiler::visit(const BlurNode &node) +{ + current_functions_->emplace(blur_function); + + *current_stream_ << "blur(" << texture_name(node.input_node()->texture(), textures_) << ", tex_coord)"; +} + +void GLSLShaderCompiler::visit(const CompositeNode &node) +{ + current_functions_->emplace(composite_function); + + *current_stream_ << "composite("; + node.colour1()->accept(*this); + *current_stream_ << ", "; + node.colour2()->accept(*this); + *current_stream_ << ", "; + node.depth1()->accept(*this); + *current_stream_ << ", "; + node.depth2()->accept(*this); + *current_stream_ << ", tex_coord)"; +} + +void GLSLShaderCompiler::visit(const VertexPositionNode &) +{ + *current_stream_ << "position"; +} + +void GLSLShaderCompiler::visit(const ValueNode &node) +{ + *current_stream_ << std::to_string(node.value()); +} + +void GLSLShaderCompiler::visit(const ValueNode &node) +{ + *current_stream_ << "vec3(" << node.value().x << ", " << node.value().y << ", " << node.value().z << ")"; +} + +void GLSLShaderCompiler::visit(const ValueNode &node) +{ + *current_stream_ << "vec4(" << node.value().g << ", " << node.value().g << ", " << node.value().b << ", " + << node.value().a << ")"; +} + +void GLSLShaderCompiler::visit(const ArithmeticNode &node) +{ + *current_stream_ << "("; + + if (node.arithmetic_operator() == ArithmeticOperator::DOT) + { + *current_stream_ << "dot("; + node.value1()->accept(*this); + *current_stream_ << ", "; + node.value2()->accept(*this); + *current_stream_ << ")"; + } + else + { + node.value1()->accept(*this); + switch (node.arithmetic_operator()) + { + case ArithmeticOperator::ADD: *current_stream_ << " + "; break; + case ArithmeticOperator::SUBTRACT: *current_stream_ << " - "; break; + case ArithmeticOperator::MULTIPLY: *current_stream_ << " * "; break; + case ArithmeticOperator::DIVIDE: *current_stream_ << " / "; break; + default: throw Exception("unknown arithmetic operator"); + } + node.value2()->accept(*this); + } + + *current_stream_ << ")"; +} + +void GLSLShaderCompiler::visit(const ConditionalNode &node) +{ + *current_stream_ << "("; + node.input_value1()->accept(*this); + + switch (node.conditional_operator()) + { + case ConditionalOperator::GREATER: *current_stream_ << " > "; break; + } + + node.input_value2()->accept(*this); + + *current_stream_ << " ? "; + node.output_value1()->accept(*this); + *current_stream_ << " : "; + node.output_value2()->accept(*this); + *current_stream_ << ")"; +} + +void GLSLShaderCompiler::visit(const ComponentNode &node) +{ + node.input_node()->accept(*this); + *current_stream_ << "." << node.component(); +} + +void GLSLShaderCompiler::visit(const CombineNode &node) +{ + *current_stream_ << "vec4("; + node.value1()->accept(*this); + *current_stream_ << ", "; + node.value2()->accept(*this); + *current_stream_ << ", "; + node.value3()->accept(*this); + *current_stream_ << ", "; + node.value4()->accept(*this); + *current_stream_ << ")"; +} + +void GLSLShaderCompiler::visit(const SinNode &node) +{ + *current_stream_ << "sin("; + node.input_node()->accept(*this); + *current_stream_ << ")"; +} + +std::string GLSLShaderCompiler::vertex_shader() const +{ + std::stringstream stream{}; + + stream << preamble << '\n'; + stream << layouts << '\n'; + stream << uniforms << '\n'; + + for (auto i = 0u; i < textures_.size(); ++i) + { + stream << "uniform sampler2D texture" << i << ";\n"; + } + + stream << vertex_out << '\n'; + + for (const auto &function : vertex_functions_) + { + stream << function << '\n'; + } + + stream << vertex_stream_.str() << '\n'; + + return stream.str(); +} + +std::string GLSLShaderCompiler::fragment_shader() const +{ + std::stringstream stream{}; + + stream << preamble << '\n'; + stream << uniforms << '\n'; + stream << "uniform sampler2D g_shadow_map;\n"; + + for (auto i = 0u; i < textures_.size(); ++i) + { + stream << "uniform sampler2D texture" << i << ";\n"; + } + + stream << fragment_in << '\n'; + stream << fragment_out << '\n'; + + for (const auto &function : fragment_functions_) + { + stream << function << '\n'; + } + + stream << fragment_stream_.str() << '\n'; + + return stream.str(); +} + +std::vector GLSLShaderCompiler::textures() const +{ + return textures_; +} +} diff --git a/src/graphics/opengl/opengl.cpp b/src/graphics/opengl/opengl.cpp new file mode 100644 index 00000000..05e088d2 --- /dev/null +++ b/src/graphics/opengl/opengl.cpp @@ -0,0 +1,31 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/opengl/opengl.h" + +#include +#include +#include +#include + +namespace iris +{ + +std::optional do_check_opengl_error(std::string_view error_message) +{ + std::optional final_message{}; + + if (const auto error = ::glGetError(); error != GL_NO_ERROR) + { + std::stringstream strm{}; + strm << error_message << " : " << error; + final_message = strm.str(); + } + + return final_message; +} + +} diff --git a/src/graphics/opengl/opengl_buffer.cpp b/src/graphics/opengl/opengl_buffer.cpp new file mode 100644 index 00000000..1aab625f --- /dev/null +++ b/src/graphics/opengl/opengl_buffer.cpp @@ -0,0 +1,141 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/opengl/opengl_buffer.h" + +#include +#include +#include + +#include "core/error_handling.h" +#include "graphics/opengl/opengl.h" +#include "graphics/vertex_data.h" + +namespace +{ + +/** + * Helper function to create an opengl buffer from a collection of objects. + * + * @param data + * Data to store in buffer. + * + * @param target + * OpenGL buffer binding target. + * + * @returns + * Handle to opengl buffer. + */ +template +GLuint create_buffer(const std::vector &data, GLenum target) +{ + GLuint handle = 0u; + + ::glGenBuffers(1, &handle); + iris::expect(iris::check_opengl_error, "could not generate opengl buffer"); + + // bind so we can copy data + ::glBindBuffer(target, handle); + iris::expect(iris::check_opengl_error, "could not bind buffer"); + + // copy data to buffer + ::glBufferData(target, data.size() * sizeof(T), data.data(), GL_STATIC_DRAW); + iris::expect(iris::check_opengl_error, "could not buffer data"); + + // unbind buffer + ::glBindBuffer(target, 0u); + iris::expect(iris::check_opengl_error, "could not unbind buffer"); + + return handle; +} + +/** + * Helper function to update a buffer with new data. + * + * @param data + * New data to store in buffer. + * + * @param target + * OpenGL buffer binding target. + * + * @param handle + * OpenGL handle to buffer to update. + * + * @param element_count + * Number of elements currently stored in buffer. + */ +template +void update_buffer(const std::vector &data, GLenum target, GLuint handle, size_t element_count) +{ + // bind so we can copy data + ::glBindBuffer(target, handle); + iris::expect(iris::check_opengl_error, "could not bind buffer"); + + if (data.size() > element_count) + { + // if we don't have enough space in buffer then create a new data store + ::glBufferData(target, data.size() * sizeof(T), data.data(), GL_STATIC_DRAW); + iris::expect(iris::check_opengl_error, "could not buffer data"); + } + else + { + // if we do have enough space then update existing data store + ::glBufferSubData(target, 0u, data.size() * sizeof(T), data.data()); + iris::expect(iris::check_opengl_error, "could not sub-buffer data"); + } + + // unbind buffer + ::glBindBuffer(target, 0u); + iris::expect(iris::check_opengl_error, "could not unbind buffer"); +} + +} + +namespace iris +{ + +OpenGLBuffer::OpenGLBuffer(const std::vector &vertex_data) + : handle_(create_buffer(vertex_data, GL_ARRAY_BUFFER)) + , element_count_(vertex_data.size()) +{ +} + +OpenGLBuffer::OpenGLBuffer(const std::vector &index_data) + : handle_(create_buffer(index_data, GL_ELEMENT_ARRAY_BUFFER)) + , element_count_(index_data.size()) +{ +} + +OpenGLBuffer::~OpenGLBuffer() +{ + ::glDeleteBuffers(1, &handle_); +} + +GLuint OpenGLBuffer::handle() const +{ + return handle_; +} + +std::size_t OpenGLBuffer::element_count() const +{ + return element_count_; +} + +void OpenGLBuffer::write(const std::vector &vertex_data) +{ + update_buffer(vertex_data, GL_ARRAY_BUFFER, handle_, element_count_); + + element_count_ = vertex_data.size(); +} + +void OpenGLBuffer::write(const std::vector &index_data) +{ + update_buffer(index_data, GL_ELEMENT_ARRAY_BUFFER, handle_, element_count_); + + element_count_ = index_data.size(); +} + +} diff --git a/src/graphics/opengl/opengl_material.cpp b/src/graphics/opengl/opengl_material.cpp new file mode 100644 index 00000000..d4f9bce6 --- /dev/null +++ b/src/graphics/opengl/opengl_material.cpp @@ -0,0 +1,117 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/opengl/opengl_material.h" + +#include +#include +#include + +#include "core/error_handling.h" +#include "graphics/opengl/glsl_shader_compiler.h" +#include "graphics/opengl/opengl.h" +#include "graphics/opengl/opengl_shader.h" +#include "graphics/render_graph/render_graph.h" +#include "graphics/shader_type.h" + +namespace +{ + +/** + * Helper function to create an opengl program. + * + * @param vertex_shader_source + * Source for vertex shader. + * + * @param fragment_shader_source + * Source for fragment shader. + * + * @returns + * Opengl program object. + */ +GLuint create_program(const std::string &vertex_shader_source, const std::string &fragment_shader_source) +{ + const auto program = ::glCreateProgram(); + iris::expect(iris::check_opengl_error, "could not create new program"); + + const iris::OpenGLShader vertex_shader{vertex_shader_source, iris::ShaderType::VERTEX}; + const iris::OpenGLShader fragment_shader{fragment_shader_source, iris::ShaderType::FRAGMENT}; + + ::glAttachShader(program, vertex_shader.native_handle()); + iris::expect(iris::check_opengl_error, "could not attach vertex shader"); + + ::glAttachShader(program, fragment_shader.native_handle()); + iris::expect(iris::check_opengl_error, "could not attach fragment shader"); + + ::glLinkProgram(program); + + GLint programparam = 0; + ::glGetProgramiv(program, GL_LINK_STATUS, &programparam); + + // if program failed to link then get the opengl error + if (programparam != GL_TRUE) + { + ::glGetProgramiv(program, GL_INFO_LOG_LENGTH, &programparam); + iris::expect(iris::check_opengl_error, "could not get program log length"); + + if (programparam == 0) + { + throw iris::Exception("program link failed: no log"); + } + else + { + std::vector error_log(programparam); + + // get opengl error log + GLsizei log_length = 0; + ::glGetProgramInfoLog(program, static_cast(error_log.size()), &log_length, error_log.data()); + iris::expect(iris::check_opengl_error, "failed to get error log"); + + const std::string error(error_log.data(), log_length); + throw iris::Exception("program link failed: " + error); + } + } + + return program; +} + +} + +namespace iris +{ + +OpenGLMaterial::OpenGLMaterial(const RenderGraph *render_graph, LightType light_type) + : handle_(0u) + , textures_() +{ + GLSLShaderCompiler compiler{render_graph, light_type}; + + handle_ = create_program(compiler.vertex_shader(), compiler.fragment_shader()); + textures_ = compiler.textures(); +} + +OpenGLMaterial::~OpenGLMaterial() +{ + ::glDeleteProgram(handle_); +} + +void OpenGLMaterial::bind() const +{ + ::glUseProgram(handle_); + expect(check_opengl_error, "could not bind program"); +} + +GLuint OpenGLMaterial::handle() const +{ + return handle_; +} + +std::vector OpenGLMaterial::textures() const +{ + return textures_; +} + +} diff --git a/src/graphics/opengl/opengl_mesh.cpp b/src/graphics/opengl/opengl_mesh.cpp new file mode 100644 index 00000000..5e671b2a --- /dev/null +++ b/src/graphics/opengl/opengl_mesh.cpp @@ -0,0 +1,146 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/opengl/opengl_mesh.h" + +#include +#include + +#include "core/error_handling.h" +#include "graphics/opengl/opengl.h" +#include "graphics/vertex_attributes.h" + +namespace +{ + +/** + * Convert an engine VertexAttributeType to an OpenGL data type. + * + * @param type + * Type to convert + * + * @returns + * Tuple of OpenGL type and boolean indiciating if the returned type is a + * flaot type. + */ +std::tuple to_opengl_format(iris::VertexAttributeType type) +{ + GLenum format = 0u; + auto is_float = true; + + switch (type) + { + case iris::VertexAttributeType::FLOAT_3: + case iris::VertexAttributeType::FLOAT_4: format = GL_FLOAT; break; + case iris::VertexAttributeType::UINT32_1: + case iris::VertexAttributeType::UINT32_4: + format = GL_UNSIGNED_INT; + is_float = false; + break; + default: throw iris::Exception("unknown vertex attribute type"); + } + + return {format, is_float}; +} + +} + +namespace iris +{ + +OpenGLMesh::OpenGLMesh( + const std::vector &vertices, + const std::vector &indices, + const VertexAttributes &attributes) + : vertex_buffer_(vertices) + , index_buffer_(indices) + , vao_(0u) +{ + // create vao + ::glGenVertexArrays(1, &vao_); + expect(check_opengl_error, "could not generate vao"); + + bind(); + + // ensure both buffers are bound for the vao + ::glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_.handle()); + expect(check_opengl_error, "could not bind buffer"); + ::glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer_.handle()); + expect(check_opengl_error, "could not bind buffer"); + + auto index = 0u; + + // define the vertex attribute data for opengl + for (const auto &attribute : attributes) + { + const auto &[type, components, _, offset] = attribute; + + ::glEnableVertexAttribArray(index); + expect(check_opengl_error, "could not enable attribute"); + + const auto &[open_gl_type, is_float] = to_opengl_format(type); + + if (is_float) + { + ::glVertexAttribPointer( + index, + static_cast(components), + open_gl_type, + GL_FALSE, + static_cast(attributes.size()), + reinterpret_cast(offset)); + expect(check_opengl_error, "could not set attributes"); + } + else + { + ::glVertexAttribIPointer( + index, + static_cast(components), + open_gl_type, + static_cast(attributes.size()), + reinterpret_cast(offset)); + expect(check_opengl_error, "could not set attributes"); + } + + ++index; + } + + unbind(); +} + +OpenGLMesh::~OpenGLMesh() +{ + ::glDeleteVertexArrays(1u, &vao_); +} + +void OpenGLMesh::update_vertex_data(const std::vector &data) +{ + vertex_buffer_.write(data); +} + +void OpenGLMesh::update_index_data(const std::vector &data) +{ + index_buffer_.write(data); +} + +GLsizei OpenGLMesh::element_count() const +{ + return static_cast(index_buffer_.element_count()); +} + +void OpenGLMesh::bind() const +{ + ::glBindVertexArray(vao_); + expect(check_opengl_error, "could not bind vao"); +} + +void OpenGLMesh::unbind() const +{ + ::glBindVertexArray(0u); + expect(check_opengl_error, "could not unbind vao"); +} + +} diff --git a/src/graphics/opengl/opengl_mesh_manager.cpp b/src/graphics/opengl/opengl_mesh_manager.cpp new file mode 100644 index 00000000..df84565d --- /dev/null +++ b/src/graphics/opengl/opengl_mesh_manager.cpp @@ -0,0 +1,28 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/opengl/opengl_mesh_manager.h" + +#include +#include +#include + +#include "graphics/mesh.h" +#include "graphics/mesh_manager.h" +#include "graphics/opengl/opengl_mesh.h" +#include "graphics/vertex_data.h" + +namespace iris +{ + +std::unique_ptr OpenGLMeshManager::create_mesh( + const std::vector &vertices, + const std::vector &indices) const +{ + return std::make_unique(vertices, indices, DefaultVertexAttributes); +} + +} diff --git a/src/graphics/opengl/opengl_render_target.cpp b/src/graphics/opengl/opengl_render_target.cpp new file mode 100644 index 00000000..a7784211 --- /dev/null +++ b/src/graphics/opengl/opengl_render_target.cpp @@ -0,0 +1,64 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/opengl/opengl_render_target.h" + +#include + +#include "core/error_handling.h" +#include "graphics/opengl/opengl.h" +#include "graphics/opengl/opengl_texture.h" + +namespace iris +{ + +OpenGLRenderTarget::OpenGLRenderTarget(std::unique_ptr colour_texture, std::unique_ptr depth_texture) + : RenderTarget(std::move(colour_texture), std::move(depth_texture)) + , handle_(0u) +{ + // create a frame buffer for our target + ::glGenFramebuffers(1, &handle_); + expect(check_opengl_error, "could not generate fbo"); + + bind(GL_FRAMEBUFFER); + + const auto colour_handle = static_cast(colour_texture_.get())->handle(); + + // set colour texture + ::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colour_handle, 0); + expect(check_opengl_error, "could not attach colour texture"); + + const auto depth_handle = static_cast(depth_texture_.get())->handle(); + + // set depth texture + ::glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depth_handle, 0); + expect(check_opengl_error, "could not attach depth texture"); + + // check everything worked + const auto status = ::glCheckFramebufferStatus(GL_FRAMEBUFFER); + expect(status == GL_FRAMEBUFFER_COMPLETE, "fbo in invalid state: " + std::to_string(status)); + + unbind(GL_FRAMEBUFFER); +} + +OpenGLRenderTarget::~OpenGLRenderTarget() +{ + ::glDeleteFramebuffers(1, &handle_); +} + +void OpenGLRenderTarget::bind(GLenum target) const +{ + ::glBindFramebuffer(target, handle_); + expect(check_opengl_error, "could not bind framebuffer"); +} + +void OpenGLRenderTarget::unbind(GLenum target) const +{ + ::glBindFramebuffer(target, 0u); + expect(check_opengl_error, "could not bind framebuffer"); +} + +} diff --git a/src/graphics/opengl/opengl_renderer.cpp b/src/graphics/opengl/opengl_renderer.cpp new file mode 100644 index 00000000..21d6c794 --- /dev/null +++ b/src/graphics/opengl/opengl_renderer.cpp @@ -0,0 +1,397 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/opengl/opengl_renderer.h" + +#include +#include +#include + +#include "core/camera.h" +#include "core/error_handling.h" +#include "core/root.h" +#include "core/vector3.h" +#include "graphics/lights/lighting_rig.h" +#include "graphics/mesh_manager.h" +#include "graphics/opengl/default_uniforms.h" +#include "graphics/opengl/opengl.h" +#include "graphics/opengl/opengl_material.h" +#include "graphics/opengl/opengl_mesh.h" +#include "graphics/opengl/opengl_render_target.h" +#include "graphics/opengl/opengl_texture.h" +#include "graphics/opengl/opengl_texture_manager.h" +#include "graphics/render_entity.h" +#include "graphics/render_graph/post_processing_node.h" +#include "graphics/render_graph/texture_node.h" +#include "graphics/render_queue_builder.h" +#include "graphics/texture_manager.h" +#include "graphics/window.h" +#include "graphics/window_manager.h" +#include "log/log.h" + +#if defined(IRIS_PLATFORM_WIN32) +#include "graphics/win32/win32_opengl_window.h" +#endif + +namespace +{ + +/** + * Helper function to setup opengl for a render pass. + * + * @param target + * RenderTarget for render pass. + */ +void render_setup(const iris::OpenGLRenderTarget *target) +{ + if (target == nullptr) + { + ::glBindFramebuffer(GL_FRAMEBUFFER, 0u); + } + else + { + ::glViewport(0, 0, target->colour_texture()->width(), target->colour_texture()->height()); + iris::expect(iris::check_opengl_error, "could not set viewport"); + + target->bind(GL_FRAMEBUFFER); + } + + // clear current target + ::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); +} + +/** + * Helper function to set uniforms for a render pass. + * + * @param uniforms + * Uniforms to set. + * + * @param camera + * Camera for current render pass. + * + * @param entity + * Entity being rendered. + * + * @param shadow_map + * RenderTarget for shadow map, maybe nullptr. + * + * @param light + * Light effecting entity. + */ +void set_uniforms( + const iris::DefaultUniforms *uniforms, + const iris::Camera *camera, + const iris::RenderEntity *entity, + const iris::RenderTarget *shadow_map, + const iris::Light *light) +{ + uniforms->projection.set_value(camera->projection()); + uniforms->view.set_value(camera->view()); + uniforms->model.set_value(entity->transform()); + uniforms->normal_matrix.set_value(entity->normal_transform()); + uniforms->light_colour.set_value(light->colour_data()); + uniforms->light_position.set_value(light->world_space_data()); + uniforms->light_attenuation.set_value(light->attenuation_data()); + + // set shadow map specific texture and uniforms, if it was provided + if ((shadow_map != nullptr) && (light->type() == iris::LightType::DIRECTIONAL)) + { + const auto *directional_light = static_cast(light); + const auto *opengl_texture = static_cast(shadow_map->depth_texture()); + const auto tex_handle = opengl_texture->handle(); + + ::glActiveTexture(opengl_texture->id()); + iris::expect(iris::check_opengl_error, "could not activate texture"); + + ::glBindTexture(GL_TEXTURE_2D, tex_handle); + iris::expect(iris::check_opengl_error, "could not bind texture``"); + + uniforms->shadow_map.set_value(opengl_texture->id() - GL_TEXTURE0); + uniforms->light_projection.set_value(directional_light->shadow_camera().projection()); + uniforms->light_view.set_value(directional_light->shadow_camera().view()); + } + + uniforms->bones.set_value(entity->skeleton().transforms()); +} + +/** + * Helper function to bind all textures for a material. + * + * @param uniforms + * Uniforms to set. + * + * @param material + * Material to bind textures for. + */ +void bind_textures(const iris::DefaultUniforms *uniforms, const iris::OpenGLMaterial *material) +{ + const auto textures = material->textures(); + for (auto i = 0u; i < textures.size(); ++i) + { + const auto *opengl_texture = static_cast(textures[i]); + const auto tex_handle = opengl_texture->handle(); + + ::glActiveTexture(opengl_texture->id()); + iris::expect(iris::check_opengl_error, "could not activate texture"); + + ::glBindTexture(GL_TEXTURE_2D, tex_handle); + iris::expect(iris::check_opengl_error, "could not bind texture``"); + + uniforms->textures[i].set_value(opengl_texture->id() - GL_TEXTURE0); + } +} + +/** + * Helper function to draw all meshes in a RenderEntity. + * + * @param entity + * RenderEntity to draw + */ +void draw_meshes(const iris::RenderEntity *entity) +{ + const auto *mesh = static_cast(entity->mesh()); + mesh->bind(); + + const auto type = entity->primitive_type() == iris::PrimitiveType::TRIANGLES ? GL_TRIANGLES : GL_LINES; + + // draw! + ::glDrawElements(type, mesh->element_count(), GL_UNSIGNED_INT, 0); + iris::expect(iris::check_opengl_error, "could not draw triangles"); + + mesh->unbind(); + + if (entity->should_render_wireframe()) + { + ::glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + } +} + +} + +namespace iris +{ + +OpenGLRenderer::OpenGLRenderer(std::uint32_t width, std::uint32_t height) + : Renderer() + , render_targets_() + , materials_() + , uniforms_() + , width_(width) + , height_(height) +{ + ::glClearColor(0.39f, 0.58f, 0.93f, 1.0f); + expect(check_opengl_error, "could not set clear colour"); + + ::glEnable(GL_DEPTH_TEST); + expect(check_opengl_error, "could not enable depth testing"); + + ::glDepthFunc(GL_LEQUAL); + expect(check_opengl_error, "could not set depth test function"); + + LOG_ENGINE_INFO("render_system", "constructed opengl renderer"); +} + +void OpenGLRenderer::set_render_passes(const std::vector &render_passes) +{ + render_passes_ = render_passes; + + // add a post processing pass + + // find the pass which renders to the screen + auto final_pass = std::find_if( + std::begin(render_passes_), + std::end(render_passes_), + [](const RenderPass &pass) { return pass.render_target == nullptr; }); + + ensure(final_pass != std::cend(render_passes_), "no final pass"); + + // deferred creating of render target to ensure this class is full + // constructed + if (post_processing_target_ == nullptr) + { + post_processing_target_ = create_render_target(width_, height_); + post_processing_camera_ = std::make_unique(CameraType::ORTHOGRAPHIC, width_, height_); + } + + post_processing_scene_ = std::make_unique(); + + // create a full screen quad which renders the final stage with the post + // processing node + auto *rg = post_processing_scene_->create_render_graph(); + rg->set_render_node(rg->create(post_processing_target_->colour_texture())); + post_processing_scene_->create_entity( + rg, + Root::mesh_manager().sprite({}), + Transform({}, {}, {static_cast(width_), static_cast(height_), 1.0})); + + // wire up this pass + final_pass->render_target = post_processing_target_; + render_passes_.emplace_back(post_processing_scene_.get(), post_processing_camera_.get(), nullptr); + + // build the render queue from the provided passes + + RenderQueueBuilder queue_builder( + [this](RenderGraph *render_graph, RenderEntity *, const RenderTarget *, LightType light_type) + { + if (materials_.count(render_graph) == 0u || materials_[render_graph].count(light_type) == 0u) + { + materials_[render_graph][light_type] = std::make_unique(render_graph, light_type); + } + + return materials_[render_graph][light_type].get(); + }, + [this](std::uint32_t width, std::uint32_t height) { return create_render_target(width, height); }); + + render_queue_ = queue_builder.build(render_passes_); + + uniforms_.clear(); + + // loop through all draw commands, for each drawn entity create a uniform + // object so they can be esily set during render + for (const auto &command : render_queue_) + { + if (command.type() == RenderCommandType::DRAW) + { + const auto *material = static_cast(command.material()); + const auto *render_entity = command.render_entity(); + + // we store uniforms per entity per material + if (!uniforms_[material][render_entity]) + { + const auto program = material->handle(); + + // create default uniforms + uniforms_[material][render_entity] = std::make_unique( + OpenGLUniform(program, "projection"), + OpenGLUniform(program, "view"), + OpenGLUniform(program, "model"), + OpenGLUniform(program, "normal_matrix", false), + OpenGLUniform(program, "light_colour", false), + OpenGLUniform(program, "light_position", false), + OpenGLUniform(program, "light_attenuation", false), + OpenGLUniform(program, "g_shadow_map", false), + OpenGLUniform(program, "light_projection", false), + OpenGLUniform(program, "light_view", false), + OpenGLUniform(program, "bones")); + + // create uniforms for each texture + for (auto i = 0u; i < material->textures().size(); ++i) + { + uniforms_[material][render_entity]->textures.emplace_back( + OpenGLUniform{program, "texture" + std::to_string(i), false}); + } + } + } + } +} + +RenderTarget *OpenGLRenderer::create_render_target(std::uint32_t width, std::uint32_t height) +{ + auto &tex_man = static_cast(Root::texture_manager()); + + const auto scale = Root::window_manager().current_window()->screen_scale(); + + render_targets_.emplace_back(std::make_unique( + std::make_unique( + DataBuffer{}, width * scale, height * scale, TextureUsage::RENDER_TARGET, tex_man.next_id()), + std::make_unique( + DataBuffer{}, width * scale, height * scale, TextureUsage::DEPTH, tex_man.next_id()))); + + return render_targets_.back().get(); +} + +void OpenGLRenderer::execute_pass_start(RenderCommand &command) +{ + const auto *target = static_cast(command.render_pass()->render_target); + + // if we have no target then we render to the default framebuffer + // else we bind the supplied target + if (target == nullptr) + { + const auto scale = Root::window_manager().current_window()->screen_scale(); + + ::glViewport(0, 0, width_ * scale, height_ * scale); + expect(check_opengl_error, "could not set viewport"); + + ::glBindFramebuffer(GL_FRAMEBUFFER, 0); + expect(check_opengl_error, "could not bind default buffer"); + } + else + { + ::glViewport(0, 0, target->colour_texture()->width(), target->colour_texture()->height()); + expect(check_opengl_error, "could not set viewport"); + + target->bind(GL_FRAMEBUFFER); + } + + // clear current target + ::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + expect(check_opengl_error, "could not clear"); +} + +void OpenGLRenderer::execute_draw(RenderCommand &command) +{ + const auto *camera = command.render_pass()->camera; + const auto *render_entity = command.render_entity(); + const auto *light = command.light(); + + static const OpenGLRenderTarget *previous_target = nullptr; + const auto *target = static_cast(command.render_pass()->render_target); + + // optimisation, we only call render_setup when the target changes + if (target != previous_target) + { + render_setup(target); + previous_target = target; + } + + static const OpenGLMaterial *previous_material = nullptr; + + auto *material = static_cast(command.material()); + + // optimisation, we only bind a material when it changes + if (material != previous_material) + { + material->bind(); + previous_material = material; + } + + // set blend mode based on light + // ambient is always rendered first (no blending) + // directional and point are always rendered after (blending) + switch (light->type()) + { + case LightType::AMBIENT: ::glDisable(GL_BLEND); break; + case LightType::DIRECTIONAL: + case LightType::POINT: + ::glEnable(GL_BLEND); + ::glBlendFunc(GL_ONE, GL_ONE); + break; + } + + if (render_entity->should_render_wireframe()) + { + ::glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + } + + const auto *uniforms = uniforms_[material][render_entity].get(); + + set_uniforms(uniforms, camera, render_entity, command.shadow_map(), light); + bind_textures(uniforms, material); + draw_meshes(render_entity); +} + +void OpenGLRenderer::execute_present(RenderCommand &) +{ +#if defined(IRIS_PLATFORM_MACOS) + ::glSwapAPPLE(); +#elif defined(IRIS_PLATFORM_WIN32) + const auto *window = static_cast(Root::window_manager().current_window()); + ::SwapBuffers(window->device_context()); +#endif +} + +} diff --git a/src/graphics/opengl/opengl_shader.cpp b/src/graphics/opengl/opengl_shader.cpp new file mode 100644 index 00000000..05bed601 --- /dev/null +++ b/src/graphics/opengl/opengl_shader.cpp @@ -0,0 +1,98 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/opengl/opengl_shader.h" + +#include +#include +#include +#include +#include + +#include "core/error_handling.h" +#include "graphics/opengl/opengl.h" +#include "graphics/shader_type.h" + +namespace iris +{ + +OpenGLShader::OpenGLShader(const std::string &source, ShaderType type) + : shader_(0u) +{ + const auto native_type = (type == ShaderType::VERTEX) ? GL_VERTEX_SHADER : GL_FRAGMENT_SHADER; + + shader_ = ::glCreateShader(native_type); + expect(check_opengl_error, "could not create vertex shader"); + + auto shader_c_str = source.data(); + + ::glShaderSource(shader_, 1, &shader_c_str, nullptr); + expect(check_opengl_error, "could not set shader source"); + + ::glCompileShader(shader_); + + GLint shader_param = 0; + + ::glGetShaderiv(shader_, GL_COMPILE_STATUS, &shader_param); + expect(check_opengl_error, "could not get shader parameter"); + + // if shader failed to compile then get the opengl error + if (shader_param != GL_TRUE) + { + ::glGetShaderiv(shader_, GL_INFO_LOG_LENGTH, &shader_param); + expect(check_opengl_error, "could not get shader log length"); + + if (shader_param == 0) + { + throw Exception("shader compilation failed: no log"); + } + else + { + std::vector error_log(shader_param); + + // get opengl error log + GLsizei log_length = 0; + ::glGetShaderInfoLog(shader_, static_cast(error_log.size()), &log_length, error_log.data()); + expect(check_opengl_error, "failed to get error log"); + + std::cout << source << std::endl; + + // convert to string and throw + const std::string error(error_log.data(), log_length); + throw Exception("shader compilation failed: " + error); + } + } +} + +OpenGLShader::~OpenGLShader() +{ + ::glDeleteShader(shader_); +} + +OpenGLShader::OpenGLShader(OpenGLShader &&other) + : shader_(0u) +{ + std::swap(shader_, other.shader_); +} + +OpenGLShader &OpenGLShader::operator=(OpenGLShader &&other) +{ + // create a new shader object to 'steal' the internal state of the supplied + // object then swap + // this ensures that the current shader is correctly deleted at the end + // of this call + OpenGLShader new_shader{std::move(other)}; + std::swap(shader_, new_shader.shader_); + + return *this; +} + +GLuint OpenGLShader::native_handle() const +{ + return shader_; +} + +} diff --git a/src/graphics/opengl/opengl_texture.cpp b/src/graphics/opengl/opengl_texture.cpp new file mode 100644 index 00000000..ee4c8b5f --- /dev/null +++ b/src/graphics/opengl/opengl_texture.cpp @@ -0,0 +1,201 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/opengl/opengl_texture.h" + +#include +#include +#include +#include +#include +#include + +#include "core/data_buffer.h" +#include "core/error_handling.h" +#include "core/resource_loader.h" +#include "graphics/opengl/opengl.h" +#include "graphics/texture_usage.h" +#include "log/log.h" + +namespace +{ + +/** + * Helper function to specify a texture suitable for the texture usage. + * usage. + * + * @param width + * Width of texture. + * + * @param height + * Height of texture. + * + * @param data_ptr + * Pointer to image data. + */ +void specify_image_texture(std::uint32_t width, std::uint32_t height, const std::byte *data_ptr) +{ + ::glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB_ALPHA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data_ptr); + iris::expect(iris::check_opengl_error, "could not set specify image texture"); + + ::glGenerateMipmap(GL_TEXTURE_2D); + iris::expect(iris::check_opengl_error, "could not generate mipmaps"); +} + +/** + * Helper function to specify a texture suitable for the data usage. + * usage. + * + * @param width + * Width of texture. + * + * @param height + * Height of texture. + * + * @param data_ptr + * Pointer to image data. + */ +void specify_data_texture(std::uint32_t width, std::uint32_t height, const std::byte *data_ptr) +{ + ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data_ptr); + iris::expect(iris::check_opengl_error, "could not set specify data texture"); + + ::glGenerateMipmap(GL_TEXTURE_2D); +} + +/** + * Helper function to specify a texture suitable for the render target usage. + * usage. + * + * @param width + * Width of texture. + * + * @param height + * Height of texture. + */ +void specify_render_target_texture(std::uint32_t width, std::uint32_t height) +{ + ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr); + iris::expect(iris::check_opengl_error, "could not set specify render target texture"); +} + +/** + * Helper function to specify a texture suitable for the depth usage. + * usage. + * + * @param width + * Width of texture. + * + * @param height + * Height of texture. + */ +void specify_depth_texture(std::uint32_t width, std::uint32_t height) +{ + ::glTexImage2D( + GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, width, height, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, nullptr); + iris::expect(iris::check_opengl_error, "could not set specify depth texture"); +} + +/** + * Helper function to create an opengl Texture from data. + * + * @param data + * Image data. This should be width * hight of pixel_format tuples. + * + * @param width + * Width of image. + * + * @param height + * Height of image. + * + * @param usage + * Texture usage. + * + * @returns + * Handle to texture. + */ +GLuint create_texture( + const iris::DataBuffer &data, + std::uint32_t width, + std::uint32_t height, + iris::TextureUsage usage, + GLuint id) +{ + auto texture = 0u; + + ::glGenTextures(1, &texture); + iris::expect(iris::check_opengl_error, "could not generate texture"); + + ::glActiveTexture(id); + iris::expect(iris::check_opengl_error, "could not activate texture"); + + ::glBindTexture(GL_TEXTURE_2D, texture); + iris::expect(iris::check_opengl_error, "could not bind texture"); + + ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); + ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); + + ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + iris::expect(iris::check_opengl_error, "could not set min filter parameter"); + + ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + iris::expect(iris::check_opengl_error, "could not set max filter parameter"); + + const float border_colour[] = {1.0f, 1.0f, 1.0f, 1.0f}; + ::glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border_colour); + iris::expect(iris::check_opengl_error, "could not set border colour"); + + // opengl requires a nullptr if there is no data + const auto *data_ptr = (data.empty()) ? nullptr : data.data(); + + switch (usage) + { + case iris::TextureUsage::IMAGE: specify_image_texture(width, height, data_ptr); break; + case iris::TextureUsage::DATA: specify_data_texture(width, height, data_ptr); break; + case iris::TextureUsage::RENDER_TARGET: specify_render_target_texture(width, height); break; + case iris::TextureUsage::DEPTH: specify_depth_texture(width, height); break; + default: throw iris::Exception("unknown usage"); + } + + return texture; +} + +} + +namespace iris +{ + +OpenGLTexture::OpenGLTexture( + const DataBuffer &data, + std::uint32_t width, + std::uint32_t height, + TextureUsage usage, + GLuint id) + : Texture(data, width, height, usage) + , handle_(0u) + , id_(id) +{ + handle_ = create_texture(data, width, height, usage, id_); + + LOG_ENGINE_INFO("texture", "loaded from data"); +} + +OpenGLTexture::~OpenGLTexture() +{ + // cleanup opengl resources + ::glDeleteTextures(1, &handle_); +} + +GLuint OpenGLTexture::handle() const +{ + return handle_; +} +GLuint OpenGLTexture::id() const +{ + return id_; +} + +} diff --git a/src/graphics/opengl/opengl_texture_manager.cpp b/src/graphics/opengl/opengl_texture_manager.cpp new file mode 100644 index 00000000..32721f41 --- /dev/null +++ b/src/graphics/opengl/opengl_texture_manager.cpp @@ -0,0 +1,63 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/opengl/opengl_texture_manager.h" + +#include +#include +#include + +#include "core/data_buffer.h" +#include "core/error_handling.h" +#include "graphics/opengl/opengl.h" +#include "graphics/opengl/opengl_texture.h" +#include "graphics/texture_manager.h" +#include "graphics/texture_usage.h" + +namespace iris +{ + +OpenGLTextureManager::OpenGLTextureManager() + : TextureManager() + , id_pool_() +{ + for (auto i = 79u; i > 0u; --i) + { + id_pool_.emplace(GL_TEXTURE0 + i); + } +} + +GLuint OpenGLTextureManager::next_id() +{ + expect(!id_pool_.empty(), "texture id pool empty"); + + const auto id = id_pool_.top(); + id_pool_.pop(); + + return id; +} + +void OpenGLTextureManager::return_id(GLuint id) +{ + id_pool_.emplace(id); +} + +std::unique_ptr OpenGLTextureManager::do_create( + const DataBuffer &data, + std::uint32_t width, + std::uint32_t height, + TextureUsage usage) +{ + return std::make_unique(data, width, height, usage, next_id()); +} + +void OpenGLTextureManager::destroy(Texture *texture) +{ + // return id of texture to the pool + return_id(static_cast(texture)->id()); +} + +} diff --git a/src/graphics/opengl/opengl_uniform.cpp b/src/graphics/opengl/opengl_uniform.cpp new file mode 100644 index 00000000..29f3bcea --- /dev/null +++ b/src/graphics/opengl/opengl_uniform.cpp @@ -0,0 +1,64 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/opengl/opengl_uniform.h" + +#include +#include +#include +#include + +#include "core/error_handling.h" +#include "core/matrix4.h" +#include "graphics/opengl/opengl.h" + +namespace iris +{ + +OpenGLUniform::OpenGLUniform(GLuint program, const std::string &name, bool ensure_exists) + : location_(-1) +{ + location_ = ::glGetUniformLocation(program, name.c_str()); + expect(check_opengl_error, "could not get uniform location"); + + if (ensure_exists) + { + expect(location_ != -1, "uniform location does not exist"); + } +} + +void OpenGLUniform::set_value(const Matrix4 &value) const +{ + ::glUniformMatrix4fv(location_, 1, GL_TRUE, reinterpret_cast(value.data())); + expect(check_opengl_error, "could not set uniform data"); +} + +void OpenGLUniform::set_value(const std::vector &value) const +{ + ::glUniformMatrix4fv( + location_, static_cast(value.size()), GL_TRUE, reinterpret_cast(value.data())); + expect(check_opengl_error, "could not set uniform data"); +} + +void OpenGLUniform::set_value(const std::array &value) const +{ + ::glUniform4fv(location_, 1u, value.data()); + expect(check_opengl_error, "could not set uniform data"); +} + +void OpenGLUniform::set_value(const std::array &value) const +{ + ::glUniform1fv(location_, 3u, value.data()); + expect(check_opengl_error, "could not set uniform data"); +} + +void OpenGLUniform::set_value(std::int32_t value) const +{ + ::glUniform1i(location_, value); + expect(check_opengl_error, "could not set uniform data"); +} + +} diff --git a/src/graphics/render_command.cpp b/src/graphics/render_command.cpp new file mode 100644 index 00000000..44dfd0b6 --- /dev/null +++ b/src/graphics/render_command.cpp @@ -0,0 +1,117 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_command.h" + +#include "graphics/lights/light.h" +#include "graphics/lights/light_type.h" +#include "graphics/material.h" +#include "graphics/render_command_type.h" +#include "graphics/render_entity.h" +#include "graphics/render_pass.h" +#include "graphics/render_target.h" + +namespace iris +{ + +RenderCommand::RenderCommand() + : type_(RenderCommandType::PASS_START) + , render_pass_(nullptr) + , material_(nullptr) + , render_entity_(nullptr) + , shadow_map_(nullptr) + , light_(nullptr) +{ +} + +RenderCommand::RenderCommand( + RenderCommandType type, + const RenderPass *render_pass, + const Material *material, + const RenderEntity *render_entity, + const RenderTarget *shadow_map, + const Light *light) + : type_(type) + , render_pass_(render_pass) + , material_(material) + , render_entity_(render_entity) + , shadow_map_(shadow_map) + , light_(light) +{ +} + +RenderCommandType RenderCommand::type() const +{ + return type_; +} + +void RenderCommand::set_type(RenderCommandType type) +{ + type_ = type; +} + +const RenderPass *RenderCommand::render_pass() const +{ + return render_pass_; +} + +void RenderCommand::set_render_pass(const RenderPass *render_pass) +{ + render_pass_ = render_pass; +} + +const Material *RenderCommand::material() const +{ + return material_; +} + +void RenderCommand::set_material(const Material *material) +{ + material_ = material; +} + +const RenderEntity *RenderCommand::render_entity() const +{ + return render_entity_; +} + +void RenderCommand::set_render_entity(const RenderEntity *render_entity) +{ + render_entity_ = render_entity; +} + +const Light *RenderCommand::light() const +{ + return light_; +} + +void RenderCommand::set_light(const Light *light) +{ + light_ = light; +} + +const RenderTarget *RenderCommand::shadow_map() const +{ + return shadow_map_; +} + +void RenderCommand::set_shadow_map(const RenderTarget *shadow_map) +{ + shadow_map_ = shadow_map; +} + +bool RenderCommand::operator==(const RenderCommand &other) const +{ + return (type_ == other.type_) && (render_pass_ == other.render_pass_) && (material_ == other.material_) && + (render_entity_ == other.render_entity_) && (light_ == other.light_) && (shadow_map_ == other.shadow_map_); +} + +bool RenderCommand::operator!=(const RenderCommand &other) const +{ + return !(*this == other); +} + +} diff --git a/src/graphics/render_entity.cpp b/src/graphics/render_entity.cpp new file mode 100644 index 00000000..15398d7d --- /dev/null +++ b/src/graphics/render_entity.cpp @@ -0,0 +1,150 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_entity.h" + +#include "core/camera_type.h" +#include "core/matrix4.h" +#include "core/quaternion.h" +#include "core/transform.h" +#include "core/vector3.h" +#include "graphics/skeleton.h" + +namespace +{ + +/** + * Helper function to create a normal transformation matrix from a model + * matrix. + * + * @param model + * The model matrix to calculate from. + * + * @returns + * Normal transformation matrix. + */ +iris::Matrix4 create_normal_transform(const iris::Matrix4 &model) +{ + auto normal = iris::Matrix4::transpose(iris::Matrix4::invert(model)); + + // remove the translation components + normal[3] = 0.0f; + normal[7] = 0.0f; + normal[11] = 0.0f; + + return normal; +} + +} + +namespace iris +{ +RenderEntity::RenderEntity(Mesh *mesh, const Vector3 &position, PrimitiveType primitive_type) + : RenderEntity(mesh, {position, {}, {1.0f}}, primitive_type) +{ +} + +RenderEntity::RenderEntity(Mesh *mesh, const Transform &transform, PrimitiveType primitive_type) + : RenderEntity(mesh, transform, Skeleton{}, primitive_type) +{ +} + +RenderEntity::RenderEntity(Mesh *mesh, const Transform &transform, Skeleton skeleton, PrimitiveType primitive_type) + : mesh_(mesh) + , transform_(transform) + , normal_() + , wireframe_(false) + , primitive_type_(primitive_type) + , skeleton_(std::move(skeleton)) + , receive_shadow_(true) +{ + normal_ = create_normal_transform(transform_.matrix()); +} + +Vector3 RenderEntity::position() const +{ + return transform_.translation(); +} + +void RenderEntity::set_position(const Vector3 &position) +{ + transform_.set_translation(position); + normal_ = create_normal_transform(transform_.matrix()); +} + +Quaternion RenderEntity::orientation() const +{ + return transform_.rotation(); +} + +void RenderEntity::set_orientation(const Quaternion &orientation) +{ + transform_.set_rotation(orientation); + normal_ = create_normal_transform(transform_.matrix()); +} + +void RenderEntity::set_scale(const Vector3 &scale) +{ + transform_.set_scale(scale); + normal_ = create_normal_transform(transform_.matrix()); +} + +Matrix4 RenderEntity::transform() const +{ + return transform_.matrix(); +} + +Matrix4 RenderEntity::normal_transform() const +{ + return normal_; +} + +Mesh *RenderEntity::mesh() const +{ + return mesh_; +} + +void RenderEntity::set_mesh(Mesh *mesh) +{ + mesh_ = mesh; +} + +bool RenderEntity::should_render_wireframe() const +{ + return wireframe_; +} + +void RenderEntity::set_wireframe(const bool wireframe) +{ + wireframe_ = wireframe; +} + +PrimitiveType RenderEntity::primitive_type() const +{ + return primitive_type_; +} + +Skeleton &RenderEntity::skeleton() +{ + return skeleton_; +} + +const Skeleton &RenderEntity::skeleton() const +{ + return skeleton_; +} + +bool RenderEntity::receive_shadow() const +{ + return receive_shadow_; +} + +void RenderEntity::set_receive_shadow(bool receive_shadow) +{ + receive_shadow_ = receive_shadow; +} + +} diff --git a/src/graphics/render_graph/CMakeLists.txt b/src/graphics/render_graph/CMakeLists.txt new file mode 100644 index 00000000..eeace02b --- /dev/null +++ b/src/graphics/render_graph/CMakeLists.txt @@ -0,0 +1,33 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/graphics/render_graph") + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/arithmetic_node.h + ${INCLUDE_ROOT}/blur_node.h + ${INCLUDE_ROOT}/colour_node.h + ${INCLUDE_ROOT}/combine_node.h + ${INCLUDE_ROOT}/component_node.h + ${INCLUDE_ROOT}/composite_node.h + ${INCLUDE_ROOT}/conditional_node.h + ${INCLUDE_ROOT}/invert_node.h + ${INCLUDE_ROOT}/node.h + ${INCLUDE_ROOT}/post_processing_node.h + ${INCLUDE_ROOT}/render_graph.h + ${INCLUDE_ROOT}/render_node.h + ${INCLUDE_ROOT}/shader_compiler.h + ${INCLUDE_ROOT}/sin_node.h + ${INCLUDE_ROOT}/texture_node.h + ${INCLUDE_ROOT}/vertex_position_node.h + arithmetic_node.cpp + blur_node.cpp + colour_node.cpp + combine_node.cpp + component_node.cpp + composite_node.cpp + conditional_node.cpp + invert_node.cpp + post_processing_node.cpp + render_graph.cpp + render_node.cpp + sin_node.cpp + texture_node.cpp + vertex_position_node.cpp) diff --git a/src/graphics/render_graph/arithmetic_node.cpp b/src/graphics/render_graph/arithmetic_node.cpp new file mode 100644 index 00000000..65869c90 --- /dev/null +++ b/src/graphics/render_graph/arithmetic_node.cpp @@ -0,0 +1,42 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_graph/arithmetic_node.h" + +#include "graphics/render_graph/node.h" +#include "graphics/render_graph/shader_compiler.h" + +namespace iris +{ + +ArithmeticNode::ArithmeticNode(Node *value1, Node *value2, ArithmeticOperator arithmetic_operator) + : value1_(value1) + , value2_(value2) + , arithmetic_operator_(arithmetic_operator) +{ +} + +void ArithmeticNode::accept(ShaderCompiler &compiler) const +{ + compiler.visit(*this); +} + +Node *ArithmeticNode::value1() const +{ + return value1_; +} + +Node *ArithmeticNode::value2() const +{ + return value2_; +} + +ArithmeticOperator ArithmeticNode::arithmetic_operator() const +{ + return arithmetic_operator_; +} + +} diff --git a/src/graphics/render_graph/blur_node.cpp b/src/graphics/render_graph/blur_node.cpp new file mode 100644 index 00000000..8125f8fe --- /dev/null +++ b/src/graphics/render_graph/blur_node.cpp @@ -0,0 +1,30 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_graph/blur_node.h" + +#include "graphics/render_graph/shader_compiler.h" +#include "graphics/render_graph/texture_node.h" + +namespace iris +{ + +BlurNode::BlurNode(TextureNode *input_node) + : input_node_(input_node) +{ +} + +void BlurNode::accept(ShaderCompiler &compiler) const +{ + compiler.visit(*this); +} + +TextureNode *BlurNode::input_node() const +{ + return input_node_; +} + +} diff --git a/src/graphics/render_graph/colour_node.cpp b/src/graphics/render_graph/colour_node.cpp new file mode 100644 index 00000000..9f6e7739 --- /dev/null +++ b/src/graphics/render_graph/colour_node.cpp @@ -0,0 +1,30 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_graph/colour_node.h" + +#include "core/colour.h" +#include "graphics/render_graph/shader_compiler.h" + +namespace iris +{ + +ColourNode::ColourNode(const Colour &colour) + : colour_(colour) +{ +} + +void ColourNode::accept(ShaderCompiler &compiler) const +{ + compiler.visit(*this); +} + +Colour ColourNode::colour() const +{ + return colour_; +} + +} diff --git a/src/graphics/render_graph/combine_node.cpp b/src/graphics/render_graph/combine_node.cpp new file mode 100644 index 00000000..2291aad0 --- /dev/null +++ b/src/graphics/render_graph/combine_node.cpp @@ -0,0 +1,47 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_graph/combine_node.h" + +#include "graphics/render_graph/shader_compiler.h" + +namespace iris +{ + +CombineNode::CombineNode(Node *value1, Node *value2, Node *value3, Node *value4) + : value1_(value1) + , value2_(value2) + , value3_(value3) + , value4_(value4) +{ +} + +void CombineNode::accept(ShaderCompiler &compiler) const +{ + compiler.visit(*this); +} + +Node *CombineNode::value1() const +{ + return value1_; +} + +Node *CombineNode::value2() const +{ + return value2_; +} + +Node *CombineNode::value3() const +{ + return value3_; +} + +Node *CombineNode::value4() const +{ + return value4_; +} + +} diff --git a/src/graphics/render_graph/component_node.cpp b/src/graphics/render_graph/component_node.cpp new file mode 100644 index 00000000..16568eb4 --- /dev/null +++ b/src/graphics/render_graph/component_node.cpp @@ -0,0 +1,38 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_graph/component_node.h" + +#include +#include + +#include "graphics/render_graph/shader_compiler.h" + +namespace iris +{ + +ComponentNode::ComponentNode(Node *input_node, const std::string &component) + : input_node_(input_node) + , component_(component) +{ +} + +void ComponentNode::accept(ShaderCompiler &compiler) const +{ + compiler.visit(*this); +} + +Node *ComponentNode::input_node() const +{ + return input_node_; +} + +std::string ComponentNode::component() const +{ + return component_; +} + +} diff --git a/src/graphics/render_graph/composite_node.cpp b/src/graphics/render_graph/composite_node.cpp new file mode 100644 index 00000000..fbd14c2e --- /dev/null +++ b/src/graphics/render_graph/composite_node.cpp @@ -0,0 +1,52 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_graph/composite_node.h" + +#include +#include + +#include "core/vector3.h" +#include "graphics/render_graph/shader_compiler.h" +#include "graphics/texture.h" + +namespace iris +{ + +CompositeNode::CompositeNode(Node *colour1, Node *colour2, Node *depth1, Node *depth2) + : colour1_(colour1) + , colour2_(colour2) + , depth1_(depth1) + , depth2_(depth2) +{ +} + +void CompositeNode::accept(ShaderCompiler &compiler) const +{ + compiler.visit(*this); +} + +Node *CompositeNode::colour1() const +{ + return colour1_; +} + +Node *CompositeNode::colour2() const +{ + return colour2_; +} + +Node *CompositeNode::depth1() const +{ + return depth1_; +} + +Node *CompositeNode::depth2() const +{ + return depth2_; +} + +} diff --git a/src/graphics/render_graph/conditional_node.cpp b/src/graphics/render_graph/conditional_node.cpp new file mode 100644 index 00000000..557247aa --- /dev/null +++ b/src/graphics/render_graph/conditional_node.cpp @@ -0,0 +1,59 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_graph/conditional_node.h" + +#include "graphics/render_graph/node.h" +#include "graphics/render_graph/shader_compiler.h" + +namespace iris +{ + +ConditionalNode::ConditionalNode( + Node *input_value1, + Node *input_value2, + Node *output_value1, + Node *output_value2, + ConditionalOperator conditional_operator) + : input_value1_(input_value1) + , input_value2_(input_value2) + , output_value1_(output_value1) + , output_value2_(output_value2) + , conditional_operator_(conditional_operator) +{ +} + +void ConditionalNode::accept(ShaderCompiler &compiler) const +{ + compiler.visit(*this); +} + +Node *ConditionalNode::input_value1() const +{ + return input_value1_; +} + +Node *ConditionalNode::input_value2() const +{ + return input_value2_; +} + +Node *ConditionalNode::output_value1() const +{ + return output_value1_; +} + +Node *ConditionalNode::output_value2() const +{ + return output_value2_; +} + +ConditionalOperator ConditionalNode::conditional_operator() const +{ + return conditional_operator_; +} + +} diff --git a/src/graphics/render_graph/invert_node.cpp b/src/graphics/render_graph/invert_node.cpp new file mode 100644 index 00000000..4ccfa709 --- /dev/null +++ b/src/graphics/render_graph/invert_node.cpp @@ -0,0 +1,31 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_graph/invert_node.h" + +#include + +#include "graphics/render_graph/shader_compiler.h" + +namespace iris +{ + +InvertNode::InvertNode(Node *input_node) + : input_node_(input_node) +{ +} + +void InvertNode::accept(ShaderCompiler &compiler) const +{ + compiler.visit(*this); +} + +Node *InvertNode::input_node() const +{ + return input_node_; +} + +} diff --git a/src/graphics/render_graph/post_processing_node.cpp b/src/graphics/render_graph/post_processing_node.cpp new file mode 100644 index 00000000..4710e31e --- /dev/null +++ b/src/graphics/render_graph/post_processing_node.cpp @@ -0,0 +1,25 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_graph/post_processing_node.h" + +#include "graphics/render_graph/shader_compiler.h" + +namespace iris +{ + +PostProcessingNode::PostProcessingNode(Node *input) + : RenderNode() +{ + set_colour_input(input); +} + +void PostProcessingNode::accept(ShaderCompiler &compiler) const +{ + compiler.visit(*this); +} + +} diff --git a/src/graphics/render_graph/render_graph.cpp b/src/graphics/render_graph/render_graph.cpp new file mode 100644 index 00000000..04bf3c79 --- /dev/null +++ b/src/graphics/render_graph/render_graph.cpp @@ -0,0 +1,34 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_graph/render_graph.h" + +#include + +#include "graphics/render_graph/node.h" +#include "graphics/render_graph/render_node.h" + +namespace iris +{ + +RenderGraph::RenderGraph() + : nodes_() +{ + nodes_.emplace_back(std::make_unique()); +} + +RenderNode *RenderGraph::render_node() const +{ + return static_cast(nodes_.front().get()); +} + +Node *RenderGraph::add(std::unique_ptr node) +{ + nodes_.emplace_back(std::move(node)); + return nodes_.back().get(); +} + +} diff --git a/src/graphics/render_graph/render_node.cpp b/src/graphics/render_graph/render_node.cpp new file mode 100644 index 00000000..99a95679 --- /dev/null +++ b/src/graphics/render_graph/render_node.cpp @@ -0,0 +1,102 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_graph/render_node.h" + +#include + +#include "graphics/render_graph/shader_compiler.h" + +namespace iris +{ + +RenderNode::RenderNode() + : colour_input_(nullptr) + , specular_power_input_(nullptr) + , specular_amount_input_(nullptr) + , normal_input_(nullptr) + , position_input_(nullptr) + , shadow_map_input_(nullptr) + , depth_only_(false) +{ +} + +void RenderNode::accept(ShaderCompiler &compiler) const +{ + compiler.visit(*this); +} + +Node *RenderNode::colour_input() const +{ + return colour_input_; +} + +void RenderNode::set_colour_input(Node *input) +{ + colour_input_ = input; +} + +Node *RenderNode::specular_power_input() const +{ + return specular_power_input_; +} + +void RenderNode::set_specular_power_input(Node *input) +{ + specular_power_input_ = input; +} + +Node *RenderNode::specular_amount_input() const +{ + return specular_amount_input_; +} + +void RenderNode::set_specular_amount_input(Node *input) +{ + specular_amount_input_ = input; +} + +Node *RenderNode::normal_input() const +{ + return normal_input_; +} + +void RenderNode::set_normal_input(Node *input) +{ + normal_input_ = input; +} + +Node *RenderNode::position_input() const +{ + return position_input_; +} + +void RenderNode::set_position_input(Node *input) +{ + position_input_ = input; +} + +Node *RenderNode::shadow_map_input() const +{ + return shadow_map_input_; +} + +void RenderNode::set_shadow_map_input(Node *input) +{ + shadow_map_input_ = input; +} + +bool RenderNode::is_depth_only() const +{ + return depth_only_; +} + +void RenderNode::set_depth_only(bool depth_only) +{ + depth_only_ = depth_only; +} + +} diff --git a/src/graphics/render_graph/sin_node.cpp b/src/graphics/render_graph/sin_node.cpp new file mode 100644 index 00000000..6975bf53 --- /dev/null +++ b/src/graphics/render_graph/sin_node.cpp @@ -0,0 +1,31 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_graph/sin_node.h" + +#include + +#include "graphics/render_graph/shader_compiler.h" + +namespace iris +{ + +SinNode::SinNode(Node *input_node) + : input_node_(input_node) +{ +} + +void SinNode::accept(ShaderCompiler &compiler) const +{ + compiler.visit(*this); +} + +Node *SinNode::input_node() const +{ + return input_node_; +} + +} diff --git a/src/graphics/render_graph/texture_node.cpp b/src/graphics/render_graph/texture_node.cpp new file mode 100644 index 00000000..01313be5 --- /dev/null +++ b/src/graphics/render_graph/texture_node.cpp @@ -0,0 +1,41 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_graph/texture_node.h" + +#include + +#include "core/root.h" +#include "core/vector3.h" +#include "graphics/render_graph/shader_compiler.h" +#include "graphics/texture.h" +#include "graphics/texture_manager.h" +#include "graphics/texture_usage.h" + +namespace iris +{ + +TextureNode::TextureNode(Texture *texture) + : texture_(texture) +{ +} + +TextureNode::TextureNode(const std::string &path, TextureUsage usage) + : texture_(Root::texture_manager().load(path, usage)) +{ +} + +void TextureNode::accept(ShaderCompiler &compiler) const +{ + compiler.visit(*this); +} + +Texture *TextureNode::texture() const +{ + return texture_; +} + +} diff --git a/src/graphics/render_graph/vertex_position_node.cpp b/src/graphics/render_graph/vertex_position_node.cpp new file mode 100644 index 00000000..1b8b3c36 --- /dev/null +++ b/src/graphics/render_graph/vertex_position_node.cpp @@ -0,0 +1,23 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_graph/vertex_position_node.h" + +#include "graphics/render_graph/shader_compiler.h" + +namespace iris +{ + +VertexPositionNode::VertexPositionNode() +{ +} + +void VertexPositionNode::accept(ShaderCompiler &compiler) const +{ + compiler.visit(*this); +} + +} diff --git a/src/graphics/render_queue_builder.cpp b/src/graphics/render_queue_builder.cpp new file mode 100644 index 00000000..40870780 --- /dev/null +++ b/src/graphics/render_queue_builder.cpp @@ -0,0 +1,187 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_queue_builder.h" + +#include +#include + +#include "graphics/render_graph/render_graph.h" +#include "graphics/render_target.h" +#include "graphics/renderer.h" + +namespace +{ + +/** + * Helper function to create and enqueue all commands for rendering a Scene with + * a given light type. + * + * @param scene + * Scene to render. + * + * @param light_type + * Type of light for the scene. + * + * @param cmd + * Command object to mutate and enqueue, this is passed in so it can be + * "pre-loaded" with the correct state. + * + * @param create_material_callback + * Callback for creating a Material object. + * + * @param render_queue + * Queue to add render commands to. + * + * @param shadow_maps + * Map of directional lights to their associated shadow map render target. + */ +void encode_light_pass_commands( + const iris::Scene *scene, + iris::LightType light_type, + iris::RenderCommand &cmd, + iris::RenderQueueBuilder::CreateMaterialCallback create_material_callback, + std::vector &render_queue, + const std::map &shadow_maps) +{ + // create commands for each entity in the scene + for (const auto &[render_graph, render_entity] : scene->entities()) + { + auto *material = + create_material_callback(render_graph, render_entity.get(), cmd.render_pass()->render_target, light_type); + cmd.set_material(material); + + // renderer implementation will handle duplicate checking + cmd.set_type(iris::RenderCommandType::UPLOAD_TEXTURE); + render_queue.push_back(cmd); + + cmd.set_render_entity(render_entity.get()); + + // light specific draw commands + switch (light_type) + { + case iris::LightType::AMBIENT: + cmd.set_type(iris::RenderCommandType::DRAW); + cmd.set_light(scene->lighting_rig()->ambient_light.get()); + render_queue.push_back(cmd); + break; + case iris::LightType::POINT: + // a draw command for each light + for (auto &light : scene->lighting_rig()->point_lights) + { + cmd.set_type(iris::RenderCommandType::DRAW); + cmd.set_light(light.get()); + render_queue.push_back(cmd); + } + break; + case iris::LightType::DIRECTIONAL: + // a draw command for each light + for (auto &light : scene->lighting_rig()->directional_lights) + { + cmd.set_type(iris::RenderCommandType::DRAW); + cmd.set_light(light.get()); + + // set shadow map in render command + if (render_entity->receive_shadow()) + { + auto *shadow_map = shadow_maps.count(light.get()) == 0u ? nullptr : shadow_maps.at(light.get()); + cmd.set_shadow_map(shadow_map); + } + + render_queue.push_back(cmd); + } + break; + } + } +} + +} + +namespace iris +{ + +RenderQueueBuilder::RenderQueueBuilder( + CreateMaterialCallback create_material_callback, + CreateRenderTargetCallback create_render_target_callback) + : create_material_callback_(create_material_callback) + , create_render_target_callback_(create_render_target_callback) +{ +} + +std::vector RenderQueueBuilder::build(std::vector &render_passes) const +{ + std::map shadow_maps; + std::vector shadow_passes{}; + + // for each shadow casting light create a render target for the shadow map + // and enqueue commands so they are rendered + for (const auto &pass : render_passes) + { + for (const auto &light : pass.scene->lighting_rig()->directional_lights) + { + if (light->casts_shadows()) + { + auto *rt = create_render_target_callback_(1024u, 1024u); + RenderPass shadow_pass = pass; + shadow_pass.camera = std::addressof(light->shadow_camera()); + shadow_pass.render_target = rt; + shadow_pass.depth_only = true; + + shadow_passes.emplace_back(shadow_pass); + + shadow_maps[light.get()] = rt; + } + } + } + + // insert shadow passes into the queue + render_passes.insert(std::cbegin(render_passes), std::cbegin(shadow_passes), std::cend(shadow_passes)); + + std::vector render_queue; + + RenderCommand cmd{}; + + // convert each pass into a series of commands which will render it + for (auto &pass : render_passes) + { + const auto has_directional_light_pass = + !pass.depth_only && !pass.scene->lighting_rig()->directional_lights.empty(); + const auto has_point_light_pass = !pass.depth_only && !pass.scene->lighting_rig()->point_lights.empty(); + + cmd.set_render_pass(std::addressof(pass)); + + cmd.set_type(RenderCommandType::PASS_START); + render_queue.push_back(cmd); + + // always encode ambient light pass + encode_light_pass_commands( + pass.scene, LightType::AMBIENT, cmd, create_material_callback_, render_queue, shadow_maps); + + // encode point lights if there are any + if (has_point_light_pass) + { + encode_light_pass_commands( + pass.scene, LightType::POINT, cmd, create_material_callback_, render_queue, shadow_maps); + } + + // encode directional lights if there are any + if (has_directional_light_pass) + { + encode_light_pass_commands( + pass.scene, LightType::DIRECTIONAL, cmd, create_material_callback_, render_queue, shadow_maps); + } + + cmd.set_type(RenderCommandType::PASS_END); + render_queue.push_back(cmd); + } + + cmd.set_type(RenderCommandType::PRESENT); + render_queue.push_back(cmd); + + return render_queue; +} + +} diff --git a/src/graphics/render_target.cpp b/src/graphics/render_target.cpp new file mode 100644 index 00000000..8b6b8e98 --- /dev/null +++ b/src/graphics/render_target.cpp @@ -0,0 +1,51 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/render_target.h" + +#include +#include + +#include "core/error_handling.h" +#include "graphics/texture.h" +#include "graphics/texture_manager.h" + +namespace iris +{ + +RenderTarget::RenderTarget(std::unique_ptr colour_texture, std::unique_ptr depth_texture) + : colour_texture_(std::move(colour_texture)) + , depth_texture_(std::move(depth_texture)) +{ + expect( + (colour_texture_->width() == depth_texture_->width()) && + (colour_texture_->height() == depth_texture_->height()), + "colour and depth dimensions must match"); +} + +RenderTarget::~RenderTarget() = default; + +Texture *RenderTarget::colour_texture() const +{ + return colour_texture_.get(); +} + +Texture *RenderTarget::depth_texture() const +{ + return depth_texture_.get(); +} + +std::uint32_t RenderTarget::width() const +{ + return colour_texture_->width(); +} + +std::uint32_t RenderTarget::height() const +{ + return colour_texture_->height(); +} + +} diff --git a/src/graphics/renderer.cpp b/src/graphics/renderer.cpp new file mode 100644 index 00000000..d51afdfa --- /dev/null +++ b/src/graphics/renderer.cpp @@ -0,0 +1,79 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/renderer.h" + +#include "core/exception.h" + +namespace iris +{ + +Renderer::Renderer() + : render_passes_() + , render_queue_() + , post_processing_scene_() + , post_processing_target_(nullptr) + , post_processing_camera_() +{ +} + +void Renderer::render() +{ + pre_render(); + + // call each command with the appropriate handler + for (auto &command : render_queue_) + { + switch (command.type()) + { + case RenderCommandType::UPLOAD_TEXTURE: execute_upload_texture(command); break; + case RenderCommandType::PASS_START: execute_pass_start(command); break; + case RenderCommandType::DRAW: execute_draw(command); break; + case RenderCommandType::PASS_END: execute_pass_end(command); break; + case RenderCommandType::PRESENT: execute_present(command); break; + default: throw Exception("unknown render queue command"); + } + } + + post_render(); +} + +void Renderer::pre_render() +{ + // default is to do nothing +} + +void Renderer::execute_upload_texture(RenderCommand &) +{ + // default is to do nothing +} + +void Renderer::execute_pass_start(RenderCommand &) +{ + // default is to do nothing +} + +void Renderer::execute_draw(RenderCommand &) +{ + // default is to do nothing +} + +void Renderer::execute_pass_end(RenderCommand &) +{ + // default is to do nothing +} + +void Renderer::execute_present(RenderCommand &) +{ + // default is to do nothing +} + +void Renderer::post_render() +{ + // default is to do nothing +} + +} diff --git a/src/graphics/scene.cpp b/src/graphics/scene.cpp new file mode 100644 index 00000000..bad87812 --- /dev/null +++ b/src/graphics/scene.cpp @@ -0,0 +1,106 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/scene.h" + +#include + +#include "core/colour.h" +#include "core/error_handling.h" +#include "graphics/lights/lighting_rig.h" +#include "graphics/render_entity.h" +#include "graphics/render_graph/render_graph.h" + +namespace iris +{ +Scene::Scene() + : entities_() + , render_graphs_() + , lighting_rig_() +{ + lighting_rig_.ambient_light = std::make_unique(Colour{1.0f, 1.0f, 1.0f}); +} + +RenderGraph *Scene::add(std::unique_ptr graph) +{ + render_graphs_.emplace_back(std::move(graph)); + return render_graphs_.back().get(); +} + +RenderEntity *Scene::add(RenderGraph *render_graph, std::unique_ptr entity) +{ + if (render_graph == nullptr) + { + static RenderGraph default_graph{}; + render_graph = &default_graph; + entity->set_receive_shadow(false); + } + + entities_.emplace_back(render_graph, std::move(entity)); + + return std::get<1>(entities_.back()).get(); +} + +void Scene::remove(RenderEntity *entity) +{ + entities_.erase( + std::remove_if( + std::begin(entities_), + std::end(entities_), + [entity](const auto &e) { return std::get<1>(e).get() == entity; }), + std::end(entities_)); +} + +PointLight *Scene::add(std::unique_ptr light) +{ + lighting_rig_.point_lights.emplace_back(std::move(light)); + return lighting_rig_.point_lights.back().get(); +} + +DirectionalLight *Scene::add(std::unique_ptr light) +{ + lighting_rig_.directional_lights.emplace_back(std::move(light)); + return lighting_rig_.directional_lights.back().get(); +} + +Colour Scene::ambient_light() const +{ + return lighting_rig_.ambient_light->colour(); +} + +void Scene::set_ambient_light(const Colour &colour) +{ + lighting_rig_.ambient_light->set_colour(colour); +} + +RenderGraph *Scene::render_graph(RenderEntity *entity) const +{ + auto found = std::find_if( + std::cbegin(entities_), + std::cend(entities_), + [entity](const auto &element) { return std::get<1>(element).get() == entity; }); + + expect(found == std::cend(entities_), "entity not in scene"); + + return std::get<0>(*found); +} + +std::vector>> &Scene::entities() +{ + return entities_; +} + +const std::vector>> &Scene::entities() const +{ + return entities_; +} + +const LightingRig *Scene::lighting_rig() const +{ + return &lighting_rig_; +} + +} diff --git a/src/graphics/skeleton.cpp b/src/graphics/skeleton.cpp new file mode 100644 index 00000000..103ee3d4 --- /dev/null +++ b/src/graphics/skeleton.cpp @@ -0,0 +1,223 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/skeleton.h" + +#include +#include +#include +#include +#include + +#include "core/error_handling.h" +#include "core/matrix4.h" +#include "graphics/animation.h" +#include "graphics/bone.h" +#include "graphics/weight.h" + +namespace +{ + +/** + * Helper function to update transformation matrices. This is done by walking + * the bone hierarchy and applying the bone transformations (optionally also + * applied animation transformations). + * + * @param transforms + * Collection to update. + * + * @param bones + * Bones to calculate. + * + * @param parents + * Index of parents of bones. + * + * @param animation + * Pointer to animation object, if not null will apply animation transforms. + */ +void update_transforms( + std::vector &transforms, + const std::vector &bones, + const std::vector &parents, + const iris::Animation *animation) +{ + // get inverse transform of root node + const auto inverse = iris::Matrix4::invert(bones.front().transform()); + + // we need some scratch space to save bone transformations as we calculate + // them, this allows us to look up a parents transform + // we don't want to update the actual bones transform as this causes issues + // when we change animation + std::vector cache(transforms.size()); + transforms[0] = bones.front().transform(); + + // walk remaining bones - these are in hierarchal order so we will always + // update a parent before its children + for (auto i = 1u; i < bones.size(); ++i) + { + auto &bone = bones[i]; + + if (bone.is_manual()) + { + // if a bone is manual then its transform is absolute, so no need + // to apply parents transform + cache[i] = bone.transform(); + transforms[i] = inverse * cache[i] * bone.offset(); + } + else if (animation != nullptr) + { + // check if our bone exists in the supplied animation + if (animation->bone_exists(bone.name())) + { + // apply parent transform with animation transform + cache[i] = cache[parents[i]] * animation->transform(bone.name()).matrix(); + transforms[i] = inverse * cache[i] * bone.offset(); + } + } + else + { + // apply parent transform + cache[i] = cache[parents[i]] * bone.transform(); + transforms[i] = inverse * cache[i] * bone.offset(); + } + } +} + +} + +namespace iris +{ + +Skeleton::Skeleton() + : Skeleton({{"root", {}, std::vector{{0u, 1.0f}}, {}, {}}}, {}) +{ +} + +Skeleton::Skeleton(std::vector bones, const std::vector &animations) + : bones_() + , parents_() + , transforms_(100) + , animations_(animations) + , current_animation_() +{ + // a root bone is one without a parent, only support one + ensure( + std::count_if(std::cbegin(bones), std::cend(bones), [](const Bone &bone) { return bone.parent().empty(); }) == + 1, + "only support one root bones"); + + auto root = + std::find_if(std::begin(bones), std::end(bones), [](const Bone &bone) { return bone.parent().empty(); }); + + // we need to copy the supplied bones in a specific order + // the aim is to flatten the hierarchy so that the root node is first, + // then its children, then grand children etc + // this ordering guarantees that a nodes always precedes its children, which + // makes updating transforms much simpler (as we can just iterate through + // the list as we know a parents transformation will be updated by the time + // we reach the child) + + std::queue::iterator> queue; + queue.emplace(root); + parents_.emplace_back(std::numeric_limits::max()); + + // breadth-first walk the hierarchy + do + { + const auto iter = queue.front(); + queue.pop(); + + // move the bone to the back of our list + std::move(iter, iter + 1u, std::back_inserter(bones_)); + + const auto name = bones_.back().name(); + + // have to search all bones for children every time, not the most + // efficient but its a one off cost + for (auto i = std::begin(bones); i != std::end(bones); ++i) + { + if (i->parent() == name) + { + queue.emplace(i); + parents_.emplace_back(bones_.size() - 1u); + } + } + + } while (!queue.empty()); + + update_transforms(transforms_, bones_, parents_, nullptr); +} + +const std::vector &Skeleton::bones() const +{ + return bones_; +} + +const std::vector &Skeleton::transforms() const +{ + return transforms_; +} + +void Skeleton::set_animation(const std::string &name) +{ + current_animation_ = name; + auto animation = std::find_if( + std::begin(animations_), + std::end(animations_), + [this](const Animation &element) { return element.name() == current_animation_; }); + + expect(animation != std::cend(animations_), "unknown animation"); + + animation->reset(); + + update_transforms(transforms_, bones_, parents_, std::addressof(*animation)); +} + +Animation &Skeleton::animation() +{ + auto animation = std::find_if( + std::begin(animations_), + std::end(animations_), + [this](const Animation &element) { return element.name() == current_animation_; }); + + return *animation; +} + +void Skeleton::advance() +{ + auto animation = std::find_if( + std::begin(animations_), + std::end(animations_), + [this](const Animation &element) { return element.name() == current_animation_; }); + + expect(animation != std::end(animations_), "unknown animation"); + + animation->advance(); + + update_transforms(transforms_, bones_, parents_, std::addressof(*animation)); +} + +std::size_t Skeleton::bone_index(const std::string &name) const +{ + const auto bone = + std::find_if(std::cbegin(bones_), std::cend(bones_), [&name](const Bone &bone) { return bone.name() == name; }); + + expect(bone != std::cend(bones_), "unknown bone"); + + return std::distance(std::cbegin(bones_), bone); +} + +Bone &Skeleton::bone(std::size_t index) +{ + return bones_[index]; +} + +const Bone &Skeleton::bone(std::size_t index) const +{ + return bones_[index]; +} + +} diff --git a/src/graphics/texture.cpp b/src/graphics/texture.cpp new file mode 100644 index 00000000..c2f79e24 --- /dev/null +++ b/src/graphics/texture.cpp @@ -0,0 +1,57 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/texture.h" + +#include + +#include "graphics/texture_usage.h" + +namespace iris +{ + +Texture::Texture(const DataBuffer &data, std::uint32_t width, std::uint32_t height, TextureUsage usage) + : data_(data) + , width_(width) + , height_(height) + , flip_(false) + , usage_(usage) +{ +} + +Texture::~Texture() = default; + +const DataBuffer &Texture::data() const +{ + return data_; +} + +std::uint32_t Texture::width() const +{ + return width_; +} + +std::uint32_t Texture::height() const +{ + return height_; +} + +TextureUsage Texture::usage() const +{ + return usage_; +} + +bool Texture::flip() const +{ + return flip_; +} + +void Texture::set_flip(bool flip) +{ + flip_ = flip; +} + +} diff --git a/src/graphics/texture_manager.cpp b/src/graphics/texture_manager.cpp new file mode 100644 index 00000000..55b3dacc --- /dev/null +++ b/src/graphics/texture_manager.cpp @@ -0,0 +1,177 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/texture_manager.h" + +#include +#include +#include +#include +#include +#include + +#define STB_IMAGE_IMPLEMENTATION +#include + +#include "core/auto_release.h" +#include "core/data_buffer.h" +#include "core/error_handling.h" +#include "core/resource_loader.h" +#include "graphics/texture.h" +#include "graphics/texture_usage.h" + +namespace +{ + +/** + * Load an image from a data buffer. + * + * @param data + * Image data. + * + * @returns + * Tuple of . + */ +std::tuple parse_image(const iris::DataBuffer &data) +{ + int width = 0; + int height = 0; + int num_channels = 0; + + // ensure that images are flipped along the y axis when loaded, this is so + // they work with what the graphics api treats as the origin + ::stbi_set_flip_vertically_on_load(true); + + // load image using stb library + iris::AutoRelease<::stbi_uc *, nullptr> raw_data( + ::stbi_load_from_memory( + reinterpret_cast(data.data()), + static_cast(data.size()), + &width, + &height, + &num_channels, + 0), + ::stbi_image_free); + + iris::ensure(raw_data && (num_channels != 0), "failed to load image"); + + // calculate the total number of bytes needed for the raw data + const auto size = width * height * num_channels; + + static constexpr auto output_channels = 4u; + + // create buffer big enough for RGBA data + iris::DataBuffer padded_data{width * height * output_channels}; + + // we only store image data as RGBA in the engine, so extend the data if + // we have less than four channels + + auto dst_ptr = padded_data.data(); + auto *src_ptr = reinterpret_cast(raw_data.get()); + const auto *end_ptr = reinterpret_cast(raw_data.get() + size); + + while (src_ptr != end_ptr) + { + // default pixel value (black with alpha) + // this allows us to memcpy over the data we do have and leaves the + // correct defaults if we have less than four channels + std::byte rgba[] = {std::byte{0x0}, std::byte{0x0}, std::byte{0x0}, std::byte{0xff}}; + + std::memcpy(rgba, src_ptr, num_channels); + std::memcpy(dst_ptr, rgba, output_channels); + + dst_ptr += output_channels; + src_ptr += num_channels; + } + + return std::make_tuple( + std::move(padded_data), static_cast(width), static_cast(height)); +} + +} + +namespace iris +{ + +Texture *TextureManager::load(const std::string &resource, TextureUsage usage) +{ + expect((usage == TextureUsage::IMAGE) || (usage == TextureUsage::DATA), "can only load IMAGE or DATA from file"); + + // check if texture has been loaded before, if not then load it + auto loaded = loaded_textures_.find(resource); + if (loaded == std::cend(loaded_textures_)) + { + const auto file_data = ResourceLoader::instance().load(resource); + auto [data, width, height] = parse_image(file_data); + + auto texture = do_create(data, width, height, usage); + + loaded_textures_[resource] = {1u, std::move(texture)}; + } + else + { + ++loaded_textures_[resource].ref_count; + } + + return loaded_textures_[resource].texture.get(); +} + +Texture *TextureManager::create(const DataBuffer &data, std::uint32_t width, std::uint32_t height, TextureUsage usage) +{ + static std::uint32_t counter = 0u; + + // create a unique name for the in-memory texture + std::stringstream strm; + strm << "!" << counter; + ++counter; + + const auto resource = strm.str(); + + auto texture = do_create(data, width, height, usage); + + loaded_textures_[resource] = {1u, std::move(texture)}; + + return loaded_textures_[resource].texture.get(); +} + +void TextureManager::unload(Texture *texture) +{ + // allow for implementation specific unloading logic + destroy(texture); + + // don't unload the static blank texture! + if (texture != blank()) + { + // find the texture that we want to unload + auto loaded = std::find_if( + std::begin(loaded_textures_), + std::end(loaded_textures_), + [texture](const auto &element) { return element.second.texture.get() == texture; }); + + expect(loaded != std::cend(loaded_textures_), "texture has not been loaded"); + + // decrement reference count and, if 0, unload + --loaded->second.ref_count; + if (loaded->second.ref_count == 0u) + { + loaded_textures_.erase(loaded); + } + } +} + +Texture *TextureManager::blank() +{ + static Texture *texture = + create({std::byte{0xff}, std::byte{0xff}, std::byte{0xff}, std::byte{0xff}}, 1u, 1u, TextureUsage::IMAGE); + + return texture; +} + +void TextureManager::destroy(Texture *) +{ +} + +} diff --git a/src/graphics/vertex_attributes.cpp b/src/graphics/vertex_attributes.cpp new file mode 100644 index 00000000..6f3d9fc2 --- /dev/null +++ b/src/graphics/vertex_attributes.cpp @@ -0,0 +1,90 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/vertex_attributes.h" + +#include +#include +#include +#include + +#include "core/exception.h" +#include "core/vector3.h" + +namespace +{ +/** + * Helper function to convert a VertexAttributeType to a tuple of: + * + * + * @param type + * Type to convert. + * + * @returns + * Tuple of . + */ +std::tuple type_information(iris::VertexAttributeType type) +{ + std::tuple info(0u, 0u); + + switch (type) + { + case iris::VertexAttributeType::FLOAT_3: info = {3u, sizeof(float)}; break; + case iris::VertexAttributeType::FLOAT_4: info = {4u, sizeof(float)}; break; + case iris::VertexAttributeType::UINT32_1: info = {1u, sizeof(std::uint32_t)}; break; + case iris::VertexAttributeType::UINT32_4: info = {4u, sizeof(std::uint32_t)}; break; + default: throw iris::Exception("unknown vertex attribute type"); + } + + return info; +} +} + +namespace iris +{ + +VertexAttributes::VertexAttributes(const std::vector &types) + : attributes_() + , size_(0u) +{ + std::size_t offset = 0u; + + for (const auto type : types) + { + const auto [components, size] = type_information(type); + attributes_.emplace_back(VertexAttribute{type, components, size, offset}); + offset += components * size; + } + + size_ = offset; +} + +std::size_t VertexAttributes::size() const +{ + return size_; +} + +VertexAttributes::const_iterator VertexAttributes::begin() const +{ + return cbegin(); +} + +VertexAttributes::const_iterator VertexAttributes::end() const +{ + return cend(); +} + +VertexAttributes::const_iterator VertexAttributes::cbegin() const +{ + return std::cbegin(attributes_); +} + +VertexAttributes::const_iterator VertexAttributes::cend() const +{ + return std::cend(attributes_); +} + +} diff --git a/src/graphics/win32/CMakeLists.txt b/src/graphics/win32/CMakeLists.txt new file mode 100644 index 00000000..0b94cb77 --- /dev/null +++ b/src/graphics/win32/CMakeLists.txt @@ -0,0 +1,12 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/graphics/win32") + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/win32_d3d12_window.h + ${INCLUDE_ROOT}/win32_opengl_window.h + ${INCLUDE_ROOT}/win32_window.h + ${INCLUDE_ROOT}/win32_window_manager.h + text_factory.cpp + win32_d3d12_window.cpp + win32_opengl_window.cpp + win32_window.cpp + win32_window_manager.cpp) diff --git a/src/graphics/win32/text_factory.cpp b/src/graphics/win32/text_factory.cpp new file mode 100644 index 00000000..67a950bf --- /dev/null +++ b/src/graphics/win32/text_factory.cpp @@ -0,0 +1,220 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/text_factory.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "core/auto_release.h" +#include "core/error_handling.h" +#include "core/root.h" +#include "graphics/texture.h" +#include "graphics/texture_manager.h" +#include "log/log.h" + +#pragma comment(lib, "d2d1.lib") +#pragma comment(lib, "dwrite") + +namespace +{ + +/** + * Helper function to convert a utf-8 string to a utf-16 string. + * + * @param str + * String to covert. + * + * @returns + * Converted string. + */ +std::wstring widen(const std::string &str) +{ + // calculate number of characters in wide string + const auto expected_size = + ::MultiByteToWideChar(CP_UTF8, MB_PRECOMPOSED, str.c_str(), static_cast(str.length()), NULL, 0); + + iris::ensure(expected_size != 0, "could not get wstring size"); + + std::wstring wide_str(expected_size, L'\0'); + + // widen + iris::ensure( + ::MultiByteToWideChar( + CP_UTF8, + MB_PRECOMPOSED, + str.c_str(), + static_cast(str.length()), + wide_str.data(), + static_cast(wide_str.length())) == expected_size, + "could not widen string"); + + return wide_str; +} + +/** + * Helper function to call Release on an object. + * + * @param ptr + * Ptr to release. + */ +template +void SafeRelease(T ptr) +{ + if (ptr != nullptr) + { + ptr->Release(); + } +} + +} + +namespace iris::text_factory +{ + +Texture *create(const std::string &font_name, const std::uint32_t size, const std::string &text, const Colour &colour) +{ + // create factory for creating Direct2d objects + ID2D1Factory *direct2d_factory = nullptr; + expect( + ::D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &direct2d_factory) == S_OK, + "failed to create d2d factory"); + + // create factory for creating DirectWrite objects + IDWriteFactory *write_factory = nullptr; + expect( + ::DWriteCreateFactory( + DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), reinterpret_cast(&write_factory)) == + S_OK, + "failed to create write factory"); + + // widen font name for win32 api calls + const auto font_name_wide = widen(font_name); + + // create object describing text format + IDWriteTextFormat *text_format = nullptr; + ensure( + write_factory->CreateTextFormat( + font_name_wide.c_str(), + NULL, + DWRITE_FONT_WEIGHT_REGULAR, + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + static_cast(size), + L"en-us", + &text_format) == S_OK, + "failed to create text format"); + + // widen text for win32 api calls + const auto text_wide = widen(text); + + // create object for text layout + // we assume a very large size, but can query after this call for actual + // size + IDWriteTextLayout *text_layout = nullptr; + expect( + write_factory->CreateTextLayout( + text_wide.c_str(), + static_cast(text_wide.length()), + text_format, + 99999.0f, + 99999.0f, + &text_layout) == S_OK, + "failed to create text layout"); + + // get actual text dimension + DWRITE_TEXT_METRICS metrics = {0}; + text_layout->GetMetrics(&metrics); + + const auto width = static_cast(metrics.width); + const auto height = static_cast(metrics.height); + + // create factory for creating WIC objects + IWICImagingFactory *image_factory = nullptr; + expect( + ::CoCreateInstance( + CLSID_WICImagingFactory2, + NULL, + CLSCTX_INPROC_SERVER, + __uuidof(IWICImagingFactory2), + reinterpret_cast(&image_factory)) == S_OK, + "failed to create image factory"); + + // create a bitmap to render text to + IWICBitmap *bitmap = nullptr; + expect( + image_factory->CreateBitmap(width, height, GUID_WICPixelFormat32bppPBGRA, WICBitmapCacheOnDemand, &bitmap) == + S_OK, + "failed to create bitmap"); + + // default render properties + const auto properties = ::D2D1::RenderTargetProperties( + D2D1_RENDER_TARGET_TYPE_DEFAULT, + ::D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED), + 0.f, + 0.f, + D2D1_RENDER_TARGET_USAGE_GDI_COMPATIBLE); + + // create a render target to render font to bitmap + AutoRelease render_target = {nullptr, &SafeRelease}; + expect( + direct2d_factory->CreateWicBitmapRenderTarget(bitmap, &properties, &render_target) == S_OK, + "failed to create render target"); + + // create a brush with supplied colour + AutoRelease brush = {nullptr, &SafeRelease}; + expect( + render_target.get()->CreateSolidColorBrush( + ::D2D1::ColorF(::D2D1::ColorF::ColorF(colour.r, colour.g, colour.b)), &brush) == S_OK, + "failed to create brush"); + + const auto origin = ::D2D1::Point2F(0.0f, 0.0f); + + // render text + render_target.get()->BeginDraw(); + render_target.get()->DrawTextLayout(origin, text_layout, brush); + render_target.get()->EndDraw(); + + // get size of bitmap + UINT bitmap_width = 0u; + UINT bitmap_height = 0u; + expect(bitmap->GetSize(&bitmap_width, &bitmap_height) == S_OK, "failed to get bitmap size"); + + WICRect rect = {0, 0, (INT)bitmap_width, (INT)bitmap_height}; + + // lock bitmap to read data + // will auto release lock at end of function scope + AutoRelease lock = {nullptr, [](IWICBitmapLock *lock) { lock->Release(); }}; + expect(bitmap->Lock(&rect, WICBitmapLockRead, &lock) == S_OK, "failed to get lock"); + + // get pointer to raw bitmap data + UINT buffer_size = 0u; + std::byte *buffer = nullptr; + expect( + lock.get()->GetDataPointer(&buffer_size, reinterpret_cast(&buffer)) == S_OK, + "failed to get data pointer"); + + DataBuffer pixel_data(buffer, buffer + buffer_size); + + // we have rendered font as BGRA, so swap red and blue + for (auto i = 0u; i < pixel_data.size(); i += 4u) + { + std::swap(pixel_data[i], pixel_data[i + 2u]); + } + + auto *texture = Root::texture_manager().create(pixel_data, width, height, TextureUsage::IMAGE); + texture->set_flip(true); + + return texture; +} +} \ No newline at end of file diff --git a/src/graphics/win32/win32_d3d12_window.cpp b/src/graphics/win32/win32_d3d12_window.cpp new file mode 100644 index 00000000..80ba795b --- /dev/null +++ b/src/graphics/win32/win32_d3d12_window.cpp @@ -0,0 +1,39 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/win32/win32_d3d12_window.h" + +#include +#include +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include + +#include "core/auto_release.h" +#include "core/colour.h" +#include "core/exception.h" +#include "events/event.h" +#include "events/quit_event.h" +#include "graphics/d3d12/d3d12_renderer.h" +#include "graphics/render_target.h" +#include "log/log.h" + +#pragma comment(lib, "Shcore.lib") + +namespace iris +{ + +Win32D3D12Window::Win32D3D12Window(std::uint32_t width, std::uint32_t height) + : Win32Window(width, height) +{ + renderer_ = std::make_unique(window_, width_, height_, screen_scale()); +} + +} diff --git a/src/graphics/win32/win32_opengl_window.cpp b/src/graphics/win32/win32_opengl_window.cpp new file mode 100644 index 00000000..218882be --- /dev/null +++ b/src/graphics/win32/win32_opengl_window.cpp @@ -0,0 +1,261 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/win32/win32_opengl_window.h" + +#include +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include + +#include "core/auto_release.h" +#include "core/error_handling.h" +#include "events/event.h" +#include "events/quit_event.h" +#define DONT_MAKE_GL_FUNCTIONS_EXTERN // get concrete function pointers for all + // opengl functions +#include "graphics/opengl/opengl.h" +#include "graphics/opengl/opengl_renderer.h" + +#pragma comment(lib, "Shcore.lib") + +// additional functions we don't want to make public +HGLRC (*wglCreateContextAttribsARB)(HDC, HGLRC, const int *); +BOOL(*wglChoosePixelFormatARB) +(HDC, const int *, const FLOAT *, UINT, int *, UINT *); +BOOL (*wglSwapIntervalEXT)(int); + +namespace +{ + +/** + * Helper function to resolve a single opengl function. + * + * @param function + * Reference to function pointer to resolve. + * + * @param name + * Name of function. + */ +template +void resolve_opengl_function(T &function, const std::string &name) +{ + const auto address = ::wglGetProcAddress(name.c_str()); + iris::ensure(address != NULL, "could not resolve: " + name); + + function = reinterpret_cast(address); +} + +/** + * Helper function to resolve all opengl functions. + */ +void resolve_global_opengl_functions() +{ + resolve_opengl_function(glDeleteBuffers, "glDeleteBuffers"); + resolve_opengl_function(glUseProgram, "glUseProgram"); + resolve_opengl_function(glBindBuffer, "glBindBuffer"); + resolve_opengl_function(glGenVertexArrays, "glGenVertexArrays"); + resolve_opengl_function(glDeleteVertexArrays, "glDeleteVertexArrays"); + resolve_opengl_function(glBindVertexArray, "glBindVertexArray"); + resolve_opengl_function(glEnableVertexAttribArray, "glEnableVertexAttribArray"); + resolve_opengl_function(glVertexAttribPointer, "glVertexAttribPointer"); + resolve_opengl_function(glVertexAttribIPointer, "glVertexAttribIPointer"); + resolve_opengl_function(glCreateProgram, "glCreateProgram"); + resolve_opengl_function(glAttachShader, "glAttachShader"); + resolve_opengl_function(glGenBuffers, "glGenBuffers"); + resolve_opengl_function(glBufferData, "glBufferData"); + resolve_opengl_function(glBufferSubData, "glBufferSubData"); + resolve_opengl_function(glLinkProgram, "glLinkProgram"); + resolve_opengl_function(glGetProgramiv, "glGetProgramiv"); + resolve_opengl_function(glGetProgramInfoLog, "glGetProgramInfoLog"); + resolve_opengl_function(glDeleteProgram, "glDeleteProgram"); + + resolve_opengl_function(glGenFramebuffers, "glGenFramebuffers"); + resolve_opengl_function(glBindFramebuffer, "glBindFramebuffer"); + resolve_opengl_function(glFramebufferTexture2D, "glFramebufferTexture2D"); + resolve_opengl_function(glCheckFramebufferStatus, "glCheckFramebufferStatus"); + resolve_opengl_function(glDeleteFramebuffers, "glDeleteFramebuffers"); + resolve_opengl_function(glGetUniformLocation, "glGetUniformLocation"); + resolve_opengl_function(glUniformMatrix4fv, "glUniformMatrix4fv"); + resolve_opengl_function(glUniform3f, "glUniform3f"); + resolve_opengl_function(glUniform1fv, "glUniform1fv"); + resolve_opengl_function(glUniform4fv, "glUniform4fv"); + resolve_opengl_function(glActiveTexture, "glActiveTexture"); + resolve_opengl_function(glUniform1i, "glUniform1i"); + resolve_opengl_function(glBlitFramebuffer, "glBlitFramebuffer"); + resolve_opengl_function(glCreateShader, "glCreateShader"); + resolve_opengl_function(glShaderSource, "glShaderSource"); + resolve_opengl_function(glCompileShader, "glCompileShader"); + resolve_opengl_function(glGetShaderiv, "glGetShaderiv"); + resolve_opengl_function(glGetShaderInfoLog, "glGetShaderInfoLog"); + resolve_opengl_function(glDeleteShader, "glDeleteShader"); + resolve_opengl_function(glGenerateMipmap, "glGenerateMipmap"); +} + +/** + * Helper function to resolve the wgl functions needed to setup an opengl + * window. + * + * We cannot create a win32 opengl window without certain wgl functions, but + * they cannot be resolved without a window. + * + * To get around this catch-22 we create a dummy window, resole the functions + * then destroy the window. We are then free to setup a proper opengl window. + * + * @param instance + * A handle to the instance of the module to be associated with the window. + */ +void resolve_wgl_functions(HINSTANCE instance) +{ + // dummy window class + WNDCLASSA wc = {}; + wc.lpfnWndProc = ::DefWindowProcA; + wc.hInstance = instance; + wc.lpszClassName = "dummy window class"; + wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; + + iris::ensure(::RegisterClassA(&wc) != 0, "could not register class"); + + // create dummy window + const iris::Win32OpenGLWindow::AutoWindow dummy_window = { + CreateWindowExA( + 0, + wc.lpszClassName, + "dummy window", + 0, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + 0, + 0, + wc.hInstance, + 0), + ::DestroyWindow}; + + iris::ensure(dummy_window, "could not create window"); + + iris::Win32OpenGLWindow::AutoDC dc = {::GetDC(dummy_window), [&dummy_window](HDC dc) { + ::ReleaseDC(dummy_window, dc); + }}; + iris::ensure(dc, "could not get dc"); + + // pixel format descriptor for dummy window + PIXELFORMATDESCRIPTOR pfd = {0}; + pfd.nSize = sizeof(pfd); + pfd.nVersion = 1; + pfd.iPixelType = PFD_TYPE_RGBA; + pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER; + pfd.cColorBits = 32; + pfd.cAlphaBits = 8; + pfd.iLayerType = PFD_MAIN_PLANE; + pfd.cDepthBits = 24; + pfd.cStencilBits = 8; + + const auto pixel_format = ::ChoosePixelFormat(dc, &pfd); + iris::ensure(pixel_format != 0, "could not choose pixel format"); + + if (::SetPixelFormat(dc, pixel_format, &pfd) == FALSE) + { + throw iris::Exception("could not set pixel format"); + } + + // get a wgl context + const iris::AutoRelease context = {::wglCreateContext(dc), ::wglDeleteContext}; + iris::ensure(context, "could not create gl context"); + + iris::ensure(::wglMakeCurrent(dc, context) == TRUE, "could not make current context"); + + // resolve our needed functions + resolve_opengl_function(wglChoosePixelFormatARB, "wglChoosePixelFormatARB"); + resolve_opengl_function(wglCreateContextAttribsARB, "wglCreateContextAttribsARB"); + resolve_opengl_function(wglSwapIntervalEXT, "wglSwapIntervalEXT"); + + ::wglMakeCurrent(dc, 0); +} + +/** + * Initialise opengl for a window. + * + * @param dc + * Device context for window. + */ +void init_opengl(HDC dc) +{ + int pixel_format_attribs[] = { + WGL_DRAW_TO_WINDOW_ARB, + GL_TRUE, + WGL_SUPPORT_OPENGL_ARB, + GL_TRUE, + WGL_DOUBLE_BUFFER_ARB, + GL_TRUE, + WGL_ACCELERATION_ARB, + WGL_FULL_ACCELERATION_ARB, + WGL_PIXEL_TYPE_ARB, + WGL_TYPE_RGBA_ARB, + WGL_COLOR_BITS_ARB, + 32, + WGL_DEPTH_BITS_ARB, + 24, + WGL_STENCIL_BITS_ARB, + 8, + 0}; + + int pixel_format = 0; + UINT num_formats = 0u; + ::wglChoosePixelFormatARB(dc, pixel_format_attribs, 0, 1, &pixel_format, &num_formats); + + iris::ensure(num_formats != 0, "could not choose pixel format"); + + // set pixel format + + PIXELFORMATDESCRIPTOR pfd; + iris::ensure(::DescribePixelFormat(dc, pixel_format, sizeof(pfd), &pfd) != 0, "could not describe pixel format"); + + iris::ensure(::SetPixelFormat(dc, pixel_format, &pfd) == TRUE, "could not set pixel format"); + + // opengl 3.3 + int gl_attribs[] = { + WGL_CONTEXT_MAJOR_VERSION_ARB, + 3, + WGL_CONTEXT_MINOR_VERSION_ARB, + 3, + WGL_CONTEXT_PROFILE_MASK_ARB, + WGL_CONTEXT_CORE_PROFILE_BIT_ARB, + 0, + }; + + const auto context = ::wglCreateContextAttribsARB(dc, 0, gl_attribs); + iris::ensure(context != NULL, "could not create gl context"); + + iris::ensure(::wglMakeCurrent(dc, context) == TRUE, "could not set make current context"); + + // disable vsync + iris::ensure(::wglSwapIntervalEXT(0) == TRUE, "could not disable vsync"); +} +} + +namespace iris +{ + +Win32OpenGLWindow::Win32OpenGLWindow(std::uint32_t width, std::uint32_t height) + : Win32Window(width, height) +{ + // initialise opengl + resolve_wgl_functions(wc_.hInstance); + init_opengl(dc_); + + // we can now resolve all our opengl functions + resolve_global_opengl_functions(); + + renderer_ = std::make_unique(width_, height_); +} + +} diff --git a/src/graphics/win32/win32_window.cpp b/src/graphics/win32/win32_window.cpp new file mode 100644 index 00000000..d9ea8d77 --- /dev/null +++ b/src/graphics/win32/win32_window.cpp @@ -0,0 +1,298 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/win32/win32_window.h" + +#include +#include +#include + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include + +#include "core/auto_release.h" +#include "core/error_handling.h" +#include "events/event.h" +#include "events/quit_event.h" + +#pragma comment(lib, "Shcore.lib") + +namespace +{ + +// as we have to provide a callback to windows for event data and there is no +// way of passing in custom data we use a global queue to store events +std::queue event_queue; + +/** + * Helper function to convert a windows key code to an engine key type. + * + * @param key_code + * Windows key code. + * + * @returns + * Engine Key. + */ +iris::Key windows_key_to_engine_Key(WPARAM key_code) +{ + iris::Key key; + + switch (key_code) + { + case 0x30: key = iris::Key::NUM_0; break; + case 0x31: key = iris::Key::NUM_1; break; + case 0x32: key = iris::Key::NUM_2; break; + case 0x33: key = iris::Key::NUM_3; break; + case 0x34: key = iris::Key::NUM_4; break; + case 0x35: key = iris::Key::NUM_5; break; + case 0x36: key = iris::Key::NUM_6; break; + case 0x37: key = iris::Key::NUM_7; break; + case 0x38: key = iris::Key::NUM_8; break; + case 0x39: key = iris::Key::NUM_9; break; + case 0x41: key = iris::Key::A; break; + case 0x42: key = iris::Key::B; break; + case 0x43: key = iris::Key::C; break; + case 0x44: key = iris::Key::D; break; + case 0x45: key = iris::Key::E; break; + case 0x46: key = iris::Key::F; break; + case 0x47: key = iris::Key::G; break; + case 0x48: key = iris::Key::H; break; + case 0x49: key = iris::Key::I; break; + case 0x4a: key = iris::Key::J; break; + case 0x4b: key = iris::Key::K; break; + case 0x4c: key = iris::Key::L; break; + case 0x4d: key = iris::Key::M; break; + case 0x4e: key = iris::Key::N; break; + case 0x4f: key = iris::Key::O; break; + case 0x50: key = iris::Key::P; break; + case 0x51: key = iris::Key::Q; break; + case 0x52: key = iris::Key::R; break; + case 0x53: key = iris::Key::S; break; + case 0x54: key = iris::Key::T; break; + case 0x55: key = iris::Key::U; break; + case 0x56: key = iris::Key::V; break; + case 0x57: key = iris::Key::W; break; + case 0x58: key = iris::Key::X; break; + case 0x59: key = iris::Key::Y; break; + case 0x5a: key = iris::Key::Z; break; + case VK_TAB: key = iris::Key::TAB; break; + case VK_SPACE: key = iris::Key::SPACE; break; + case VK_ESCAPE: key = iris::Key::ESCAPE; break; + case VK_LSHIFT: key = iris::Key::SHIFT; break; + case VK_RSHIFT: key = iris::Key::RIGHT_SHIFT; break; + case VK_F17: key = iris::Key::F17; break; + case VK_DECIMAL: key = iris::Key::KEYPAD_DECIMAL; break; + case VK_MULTIPLY: key = iris::Key::KEYPAD_MULTIPLY; break; + case VK_OEM_PLUS: key = iris::Key::KEYPAD_PLUS; break; + case VK_VOLUME_UP: key = iris::Key::VOLUME_UP; break; + case VK_VOLUME_DOWN: key = iris::Key::VOLUME_DOWN; break; + case VK_VOLUME_MUTE: key = iris::Key::MUTE; break; + case VK_DIVIDE: key = iris::Key::KEYPAD_DIVIDE; break; + case VK_OEM_MINUS: key = iris::Key::KEYPAD_MINUS; break; + case VK_F18: key = iris::Key::F18; break; + case VK_F19: key = iris::Key::F19; break; + case VK_NUMPAD0: key = iris::Key::KEYPAD_0; break; + case VK_NUMPAD1: key = iris::Key::KEYPAD_1; break; + case VK_NUMPAD2: key = iris::Key::KEYPAD_2; break; + case VK_NUMPAD3: key = iris::Key::KEYPAD_3; break; + case VK_NUMPAD4: key = iris::Key::KEYPAD_4; break; + case VK_NUMPAD5: key = iris::Key::KEYPAD_5; break; + case VK_NUMPAD6: key = iris::Key::KEYPAD_6; break; + case VK_NUMPAD7: key = iris::Key::KEYPAD_7; break; + case VK_F20: key = iris::Key::F20; break; + case VK_NUMPAD8: key = iris::Key::KEYPAD_8; break; + case VK_NUMPAD9: key = iris::Key::KEYPAD_9; break; + case VK_F5: key = iris::Key::F5; break; + case VK_F6: key = iris::Key::F6; break; + case VK_F7: key = iris::Key::F7; break; + case VK_F3: key = iris::Key::F3; break; + case VK_F8: key = iris::Key::F8; break; + case VK_F9: key = iris::Key::F9; break; + case VK_F11: key = iris::Key::F11; break; + case VK_F13: key = iris::Key::F13; break; + case VK_F16: key = iris::Key::F16; break; + case VK_F14: key = iris::Key::F14; break; + case VK_F10: key = iris::Key::F10; break; + case VK_F12: key = iris::Key::F12; break; + case VK_F15: key = iris::Key::F15; break; + case VK_HELP: key = iris::Key::HELP; break; + case VK_HOME: key = iris::Key::HOME; break; + case VK_F4: key = iris::Key::F4; break; + case VK_END: key = iris::Key::END; break; + case VK_F2: key = iris::Key::F2; break; + case VK_F1: key = iris::Key::F1; break; + case VK_LEFT: key = iris::Key::LEFT_ARROW; break; + case VK_RIGHT: key = iris::Key::RIGHT_ARROW; break; + case VK_DOWN: key = iris::Key::DOWN_ARROW; break; + case VK_UP: key = iris::Key::UP_ARROW; break; + default: key = iris::Key::UNKNOWN; + } + + return key; +} + +/** + * Callback function for windows events. + * + * See: + * https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms633573(v=vs.85) + * for details + */ +LRESULT CALLBACK window_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + LRESULT result = 0; + + switch (uMsg) + { + case WM_CLOSE: event_queue.emplace(iris::QuitEvent{}); break; + case WM_KEYDOWN: + event_queue.emplace(iris::KeyboardEvent{windows_key_to_engine_Key(wParam), iris::KeyState::DOWN}); + break; + case WM_KEYUP: + event_queue.emplace(iris::KeyboardEvent{windows_key_to_engine_Key(wParam), iris::KeyState::UP}); + break; + case WM_INPUT: + { + UINT dwSize = sizeof(RAWINPUT); + BYTE lpb[sizeof(RAWINPUT)]; + + ::GetRawInputData(reinterpret_cast(lParam), RID_INPUT, lpb, &dwSize, sizeof(RAWINPUTHEADER)); + + RAWINPUT raw = {0}; + std::memcpy(&raw, lpb, sizeof(raw)); + + if (raw.header.dwType == RIM_TYPEMOUSE) + { + // get mouse delta from raw input data + int x = raw.data.mouse.lLastX; + int y = raw.data.mouse.lLastY; + + event_queue.emplace(iris::MouseEvent{static_cast(x), static_cast(y)}); + } + break; + } + default: result = ::DefWindowProc(hWnd, uMsg, wParam, lParam); + } + + return result; +} + +} + +namespace iris +{ + +Win32Window::Win32Window(std::uint32_t width, std::uint32_t height) + : Window(width, height) + , window_() + , dc_() + , wc_() +{ + // ensure process is aware of high dpi monitors + ensure(::SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE) == S_OK, "could not set process dpi awareness"); + + const auto instance = ::GetModuleHandleA(NULL); + + // create window class + wc_ = {}; + wc_.lpfnWndProc = window_proc; + wc_.hInstance = instance; + wc_.lpszClassName = "window class"; + wc_.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; + + ensure(::RegisterClassA(&wc_) != 0, "could not register class"); + + // create RECT for specified window size + RECT rect = {0}; + rect.right = static_cast(width_); + rect.bottom = static_cast(height_); + + ensure(::AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, false) != 0, "could not resize window"); + + // create window, we will resize it after for current dpi + window_ = { + CreateWindowExA( + 0, + wc_.lpszClassName, + "iris", + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + rect.right - rect.left, + rect.bottom - rect.top, + NULL, + NULL, + wc_.hInstance, + NULL), + ::DestroyWindow}; + ensure(window_, "could not create window"); + + const auto scale = screen_scale(); + + // ensure window size is correctly scaled for current dpi + ensure( + ::SetWindowPos(window_, window_, 0, 0, width_ * scale, height_ * scale, SWP_NOZORDER | SWP_NOACTIVATE) != 0, + "could not set window position"); + + dc_ = {::GetDC(window_), [this](HDC dc) { ::ReleaseDC(window_, dc); }}; + ensure(dc_, "could not get dc"); + + ::ShowWindow(window_, SW_SHOW); + ::UpdateWindow(window_); + + // register for raw mouse events + RAWINPUTDEVICE rid; + rid.usUsagePage = HID_USAGE_PAGE_GENERIC; + rid.usUsage = HID_USAGE_GENERIC_MOUSE; + rid.dwFlags = RIDEV_INPUTSINK; + rid.hwndTarget = window_; + + ensure(::RegisterRawInputDevices(&rid, 1, sizeof(rid)) == TRUE, "could not register raw input device"); + + // ensure mouse visibility reference count is 0 (mouse is hidden) + while (::ShowCursor(FALSE) >= 0) + { + } +} + +std::uint32_t Win32Window::screen_scale() const +{ + const auto dpi = ::GetDpiForWindow(window_); + + return static_cast(std::ceil(static_cast(dpi) / 96.0f)); +} + +std::optional Win32Window::pump_event() +{ + // non-blocking loop to drain all available windows messages + MSG message = {0}; + while (::PeekMessageA(&message, NULL, 0, 0, PM_REMOVE) != 0) + { + ::TranslateMessage(&message); + ::DispatchMessageA(&message); + } + + std::optional event; + + // get next engine event if one exists + if (!event_queue.empty()) + { + event = event_queue.front(); + event_queue.pop(); + } + + return event; +} + +HDC Win32Window::device_context() const +{ + return dc_; +} + +} diff --git a/src/graphics/win32/win32_window_manager.cpp b/src/graphics/win32/win32_window_manager.cpp new file mode 100644 index 00000000..e8429b20 --- /dev/null +++ b/src/graphics/win32/win32_window_manager.cpp @@ -0,0 +1,49 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/win32/win32_window_manager.h" + +#include +#include + +#include "core/error_handling.h" +#include "core/root.h" +#include "graphics/win32/win32_d3d12_window.h" +#include "graphics/win32/win32_opengl_window.h" +#include "graphics/window.h" +#include "graphics/window_manager.h" + +namespace iris +{ + +Window *Win32WindowManager::create_window(std::uint32_t width, std::uint32_t height) +{ + // only support onw window at the moment + ensure(!current_window_, "window already created"); + + const auto graphics_api = Root::graphics_api(); + + if (graphics_api == "d3d12") + { + current_window_ = std::make_unique(width, height); + } + else if (graphics_api == "opengl") + { + current_window_ = std::make_unique(width, height); + } + else + { + throw Exception("unknown graphics api"); + } + + return current_window_.get(); +} + +Window *Win32WindowManager::current_window() const +{ + return current_window_.get(); +} +} diff --git a/src/graphics/window.cpp b/src/graphics/window.cpp new file mode 100644 index 00000000..8a86ae8a --- /dev/null +++ b/src/graphics/window.cpp @@ -0,0 +1,54 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "graphics/window.h" + +#include +#include + +#include "graphics/render_pass.h" +#include "graphics/render_target.h" + +namespace iris +{ +Window::Window(std::uint32_t width, std::uint32_t height) + : width_(width) + , height_(height) + , renderer_(nullptr) +{ +} + +void Window::render() const +{ + renderer_->render(); +} + +std::uint32_t Window::width() const +{ + return width_; +} + +std::uint32_t Window::height() const +{ + return height_; +} + +RenderTarget *Window::create_render_target() +{ + return create_render_target(width_, height_); +} + +RenderTarget *Window::create_render_target(std::uint32_t width, std::uint32_t height) +{ + return renderer_->create_render_target(width, height); +} + +void Window::set_render_passes(const std::vector &render_passes) +{ + renderer_->set_render_passes(render_passes); +} + +} diff --git a/src/iris-config.cmake b/src/iris-config.cmake new file mode 100644 index 00000000..6e4f07de --- /dev/null +++ b/src/iris-config.cmake @@ -0,0 +1,4 @@ +include(CMakeFindDependencyMacro) + +# add the targets file +include("${CMAKE_CURRENT_LIST_DIR}/iris-targets.cmake") diff --git a/src/jobs/CMakeLists.txt b/src/jobs/CMakeLists.txt new file mode 100644 index 00000000..df8c9736 --- /dev/null +++ b/src/jobs/CMakeLists.txt @@ -0,0 +1,11 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/jobs") + +add_subdirectory("fiber") +add_subdirectory("thread") + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/concurrent_queue.h + ${INCLUDE_ROOT}/context.h + ${INCLUDE_ROOT}/job.h + ${INCLUDE_ROOT}/job_system.h + ${INCLUDE_ROOT}/job_system_manager.h) diff --git a/src/jobs/fiber/CMakeLists.txt b/src/jobs/fiber/CMakeLists.txt new file mode 100644 index 00000000..d111135a --- /dev/null +++ b/src/jobs/fiber/CMakeLists.txt @@ -0,0 +1,15 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/jobs/fiber") + +if(IRIS_PLATFORM MATCHES "MACOS") + add_subdirectory("posix") +elseif(IRIS_PLATFORM MATCHES "WIN32") + add_subdirectory("windows") +endif() + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/counter.h + ${INCLUDE_ROOT}/fiber.h + ${INCLUDE_ROOT}/fiber_job_system.h + counter.cpp + fiber_job_system.cpp + fiber_job_system_manager.cpp) diff --git a/src/jobs/fiber/counter.cpp b/src/jobs/fiber/counter.cpp new file mode 100644 index 00000000..97189a2a --- /dev/null +++ b/src/jobs/fiber/counter.cpp @@ -0,0 +1,42 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "jobs/fiber/counter.h" + +#include +#include + +namespace iris +{ + +Counter::Counter(int value) + : value_(value) + , mutex_() +{ +} + +Counter::operator int() +{ + std::unique_lock lock(mutex_); + + return value_; +} + +void Counter::operator--() +{ + std::unique_lock lock(mutex_); + + --value_; +} + +void Counter::operator--(int) +{ + std::unique_lock lock(mutex_); + + --value_; +} + +} diff --git a/src/jobs/fiber/fiber_job_system.cpp b/src/jobs/fiber/fiber_job_system.cpp new file mode 100644 index 00000000..d3d5bf8e --- /dev/null +++ b/src/jobs/fiber/fiber_job_system.cpp @@ -0,0 +1,277 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "jobs/fiber/fiber_job_system.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/auto_release.h" +#include "core/semaphore.h" +#include "core/thread.h" +#include "jobs/concurrent_queue.h" +#include "jobs/fiber/counter.h" +#include "jobs/fiber/fiber.h" +#include "jobs/job.h" +#include "log/log.h" + +namespace +{ + +/** + * This is the main function for the worker threads. It's responsible for + * taking fibers off the queue, executing them and performing all necessary + * bookkeeping. + * + * @param id + * Unique id for thread. + * + * @param jobs_semaphore + * Semaphore signaling how many fibers are available to run. + * + * @param running + * Flag to indicate if this thread should keep running. + * + * @param fibers + * Queue of fibers to pop from.e + */ +void job_thread( + [[maybe_unused]] int id, + iris::Semaphore &jobs_semaphore, + std::atomic &running, + iris::ConcurrentQueue> &fibers) +{ + iris::Fiber::thread_to_fiber(); + + LOG_DEBUG("job_system", "{} thread start [{}]", id, (void *)*iris::Fiber::this_fiber()); + + while (running) + { + // wait for jobs to become available + jobs_semaphore.acquire(); + + if (!running) + { + break; + } + + // block and wait for fiber from queue, this should be low contention + // as most of the waiting is handled by the semaphore + auto [fiber, wait_counter] = fibers.dequeue(); + + // we cannot safely use a fiber whilst it is resuming + // as a fiber should never be in the resuming state for long (the time + // it takes to suspend and return to previous context) we use a + // primitive spin lock + while (!fiber->is_safe()) + { + } + + if (wait_counter == nullptr) + { + // if we have no wait counter then this is the first time we are + // seeing this fibre - so start it + fiber->start(); + } + else + { + // if we have a wait counter then this is a suspended fiber thats + // been put back on the queue + + if (*wait_counter == 0) + { + // wait counter of zero means all its children jobs have + // finished so we can resume + fiber->resume(); + + // if nothing is waiting on us then we were a fire-and-forget + // job so need to cleanup + if (!fiber->is_being_waited_on()) + { + delete fiber; + } + } + else + { + // we are still waiting on at least one child job to finish so + // put the fiber back on the queue + fibers.enqueue(fiber, wait_counter); + jobs_semaphore.release(); + } + } + } + + LOG_DEBUG("job_system", "{} thread end [{}]", id, (void *)*iris::Fiber::this_fiber()); + + // safe to cleanup fiber we created for thread + delete *iris::Fiber::this_fiber(); + *iris::Fiber::this_fiber() = nullptr; +} + +/** + * If the main thread (which is not a fiber) wants to wait on a job then it + * cannot. We bootstrap that by using traditional signaling primitives. + * + * @param jobs + * Jobs to wait on + * + * @param js + * Pointer to JoSystem. + */ +void bootstrap_first_job(const std::vector &jobs, iris::FiberJobSystem *js) +{ + std::mutex m; + std::condition_variable cv; + std::atomic done = false; + std::exception_ptr exception; + + // wrap everything up in a fire-and-forget job + js->add_jobs({{[&cv, &done, &jobs, &exception, js]() + { + LOG_ENGINE_INFO("job_system", "bootstrap started"); + + try + { + // we can now call wait for jobs because we are + // within another fiber + js->wait_for_jobs(jobs); + } + catch (...) + { + // capture any exception + exception = std::current_exception(); + } + + // signal calling thread we are finished + done = true; + cv.notify_one(); + LOG_ENGINE_INFO("job_system", "bootstrap lambda done"); + }}}); + + // block and wait for wrapping fiber to finish + if (!done) + { + std::unique_lock lock(m); + cv.wait(lock, [&done]() { return done.load(); }); + } + + LOG_ENGINE_INFO("job_system", "non-fiber wait complete"); + + // rethrow any exception + if (exception) + { + std::rethrow_exception(exception); + } +} + +} + +namespace iris +{ + +FiberJobSystem::FiberJobSystem() + : running_(true) + , jobs_semaphore_() + , workers_() + , fibers_() +{ + auto thread_count = std::max(1u, std::thread::hardware_concurrency() - 1u); + + LOG_ENGINE_INFO("job_system", "creating {} threads", thread_count); + for (auto i = 0u; i < thread_count; ++i) + { + workers_.emplace_back(job_thread, i + 1, std::ref(jobs_semaphore_), std::ref(running_), std::ref(fibers_)); + } +} + +FiberJobSystem::~FiberJobSystem() +{ + running_ = false; + + for (auto i = 0u; i < workers_.size() + 1u; i++) + { + jobs_semaphore_.release(); + } + + for (auto &worker : workers_) + { + worker.join(); + } +} + +void FiberJobSystem::add_jobs(const std::vector &jobs) +{ + for (const auto &job : jobs) + { + // we rely on the worker thread to clean up after us + auto *f = new Fiber{job}; + + fibers_.enqueue(f, nullptr); + jobs_semaphore_.release(); + } +} + +void FiberJobSystem::wait_for_jobs(const std::vector &jobs) +{ + if (*Fiber::this_fiber() == nullptr) + { + bootstrap_first_job(jobs, this); + } + else + { + auto counter = std::make_unique(static_cast(jobs.size())); + std::vector> fibers{}; + + // create fibers and add to the queue + for (const auto &job : jobs) + { + fibers.emplace_back(std::make_unique(job, counter.get())); + + fibers_.enqueue(fibers.back().get(), nullptr); + jobs_semaphore_.release(); + } + + // mark current fiber as unsafe (so another thread doesn't preemptively + // try to resume it), stick it on the queue + (*Fiber::this_fiber())->set_unsafe(); + fibers_.enqueue(*Fiber::this_fiber(), counter.get()); + jobs_semaphore_.release(); + + // suspend current thread - this will internally mark the fiber as safe + (*Fiber::this_fiber())->suspend(); + + // the above line will not return + // if we get there then all children jobs have finished and resume has + // been called + + std::exception_ptr job_exception; + + for (const auto &fiber : fibers) + { + // find first exception that was throw, first come first served + if ((fiber->exception() != nullptr) && !job_exception) + { + job_exception = fiber->exception(); + break; + } + } + + if (job_exception) + { + std::rethrow_exception(job_exception); + } + } +} + +} diff --git a/src/jobs/fiber/fiber_job_system_manager.cpp b/src/jobs/fiber/fiber_job_system_manager.cpp new file mode 100644 index 00000000..f469b921 --- /dev/null +++ b/src/jobs/fiber/fiber_job_system_manager.cpp @@ -0,0 +1,37 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "jobs/fiber/fiber_job_system_manager.h" + +#include +#include + +#include "core/error_handling.h" +#include "jobs/fiber/fiber_job_system.h" +#include "jobs/job.h" +#include "jobs/job_system_manager.h" + +namespace iris +{ + +JobSystem *FiberJobSystemManager::create_job_system() +{ + ensure(!job_system_, "job system already created"); + + job_system_ = std::make_unique(); + return job_system_.get(); +} + +void FiberJobSystemManager::add(const std::vector &jobs) +{ + job_system_->add_jobs(jobs); +} + +void FiberJobSystemManager::wait(const std::vector &jobs) +{ + job_system_->wait_for_jobs(jobs); +} +} diff --git a/src/jobs/fiber/posix/CMakeLists.txt b/src/jobs/fiber/posix/CMakeLists.txt new file mode 100644 index 00000000..a41a8f6d --- /dev/null +++ b/src/jobs/fiber/posix/CMakeLists.txt @@ -0,0 +1,6 @@ +set(ARCH_ROOT "${PROJECT_SOURCE_DIR}/include/iris/jobs/arch/x86_64") + +target_sources(iris PRIVATE + fiber.cpp + ${ARCH_ROOT}/context.h + ${ARCH_ROOT}/functions.S) diff --git a/src/jobs/fiber/posix/fiber.cpp b/src/jobs/fiber/posix/fiber.cpp new file mode 100644 index 00000000..923e3a87 --- /dev/null +++ b/src/jobs/fiber/posix/fiber.cpp @@ -0,0 +1,234 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "jobs/fiber/fiber.h" + +#include +#include +#include +#include +#include + +#include "core/error_handling.h" +#include "core/static_buffer.h" +#include "jobs/context.h" +#include "jobs/job.h" +#include "log/log.h" + +extern "C" +{ + // these will be defined with arch specific assembler + extern void save_context(iris::Context *); + extern void restore_context(iris::Context *); + extern void change_stack(void *stack); +} + +namespace iris +{ + +struct Fiber::implementation +{ + std::unique_ptr stack_buffer; + std::byte *stack; + Context context; + Context suspended_context; + + // all these functions are noinline and noopt + // we require a very strict ordering of instructions in order to save and + // restore contexts and we don't want the compiler to alter that + + /** + * Starts a Fiber. Once completed will restore the previously saved context. + * + * @param fiber + * Fiber to start. + */ + __attribute__((noinline, optnone)) static void do_start(Fiber *fiber) + { + try + { + // swap to new stack and execute job + change_stack(fiber->impl_->stack); + fiber->job_(); + } + catch (...) + { + // store any exceptions so it can possibly be rethrown later + if (fiber->exception_ == nullptr) + { + fiber->exception_ = std::current_exception(); + } + } + + *this_fiber() = fiber->parent_fiber_; + + restore_context(&fiber->impl_->context); + } + + /** + * Suspend the Fiber. + * + * @param fiber + * Fiber to suspend. + */ + __attribute__((noinline, optnone)) static void do_suspend(Fiber *fiber) + { + // no code between these lines + // restoring this saved context will cause execution to continue from + // *after* the following line + save_context(&fiber->impl_->suspended_context); + restore_context(&fiber->impl_->context); + + // the above call will not return + // we get here when the suspended context is restored + } + + /** + * Resume the Fiber. + * + * @param fiber + * Fiber to resume. + * + * @returns + * Always returns 0 - this is to prevent tail-call optimisation which + * can mess up the assembly calls. + */ + __attribute__((noinline, optnone)) static int do_resume(Fiber *fiber) + { + // store context so we can resume from here once our job has finished + save_context(&fiber->impl_->context); + restore_context(&fiber->impl_->suspended_context); + + return 0; + } +}; + +Fiber::Fiber(Job job) + : Fiber(job, nullptr) +{ +} + +Fiber::Fiber(Job job, Counter *counter) + : job_(nullptr) + , counter_(counter) + , parent_fiber_(nullptr) + , exception_(nullptr) + , safe_(true) + , impl_(std::make_unique()) +{ + job_ = job; + + impl_->stack_buffer = std::make_unique(10u); + + // stack grows from high -> low memory so move our pointer down, not all the + // way as we need some space to copy the previous stack frame + impl_->stack = *impl_->stack_buffer + (StaticBuffer::page_size() * 9); +} + +Fiber::~Fiber() = default; + +void Fiber::start() +{ + // bookkeeping + parent_fiber_ = *this_fiber(); + *this_fiber() = this; + + // save our context and kick off the job, we will return from here when the + // job is done (but possible on a different thread) + save_context(&impl_->context); + implementation::do_start(this); + + if (!safe_) + { + // we are no longer suspending if we are here i.e. it is now safe for + // another thread to pick us up + safe_ = true; + } + else + { + // update counter if another fiber was waiting on us + if (counter_ != nullptr) + { + (*counter_)--; + } + } +} + +void Fiber::suspend() +{ + *this_fiber() = parent_fiber_; + + implementation::do_suspend(this); + + // if we get here then we have been resumed, so rethrow any stored + // exception + if (exception_) + { + std::rethrow_exception(exception_); + } +} + +void Fiber::resume() +{ + // bookkeeping + parent_fiber_ = *this_fiber(); + *this_fiber() = this; + + implementation::do_resume(this); + + if (!safe_) + { + // we are no longer suspending if we are here i.e. it is now safe for + // another thread to pick us up + safe_ = true; + } + else + { + // update counter if another fiber was waiting on us + if (counter_ != nullptr) + { + (*counter_)--; + } + } +} + +bool Fiber::is_safe() const +{ + return safe_; +} + +void Fiber::set_unsafe() +{ + safe_ = false; +} + +bool Fiber::is_being_waited_on() const +{ + return counter_ != nullptr; +} + +std::exception_ptr Fiber::exception() const +{ + return exception_; +} + +void Fiber::thread_to_fiber() +{ + expect(*this_fiber() == nullptr, "thread already a fiber"); + + // thread will clean this up when it ends + *this_fiber() = new Fiber(nullptr); +} + +Fiber **Fiber::this_fiber() +{ + // this allows us to get a pointer to the fiber being executed in the + // current thread + thread_local Fiber *current_fiber = nullptr; + return ¤t_fiber; +} + +} diff --git a/src/jobs/fiber/windows/CMakeLists.txt b/src/jobs/fiber/windows/CMakeLists.txt new file mode 100644 index 00000000..1f0cdaef --- /dev/null +++ b/src/jobs/fiber/windows/CMakeLists.txt @@ -0,0 +1 @@ +target_sources(iris PRIVATE fiber.cpp) diff --git a/src/jobs/fiber/windows/fiber.cpp b/src/jobs/fiber/windows/fiber.cpp new file mode 100644 index 00000000..cdcf1aac --- /dev/null +++ b/src/jobs/fiber/windows/fiber.cpp @@ -0,0 +1,188 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "jobs/fiber/fiber.h" + +#include +#include + +#include + +#include "core/auto_release.h" +#include "core/error_handling.h" +#include "jobs/job.h" + +namespace iris +{ + +// we disable optimisations for job_runner as the inlining messes up with the +// fiber resuming code +#pragma optimize("", off) +struct Fiber::implementation +{ + AutoRelease handle; + + /** + * Start function for win32 fiber. + * + * @param data + * Data passed to start function. + */ + static void job_runner(void *data) + { + + auto *fiber = static_cast(data); + *this_fiber() = fiber; + + try + { + fiber->job_(); + } + catch (...) + { + // store any exceptions so it can possibly be rethrown later + if (fiber->exception_ == nullptr) + { + fiber->exception_ = std::current_exception(); + } + } + + fiber->parent_fiber_->resume(); + } +}; +#pragma optimize("", on) + +Fiber::Fiber(Job job) + : Fiber(job, nullptr) +{ +} + +Fiber::Fiber(Job job, Counter *counter) + : job_() + , counter_(counter) + , parent_fiber_(nullptr) + , exception_(nullptr) + , safe_(true) + , impl_(std::make_unique()) +{ + job_ = job; + impl_->handle = { + ::CreateFiberEx(0, 0, FIBER_FLAG_FLOAT_SWITCH, implementation::job_runner, static_cast(this)), + ::DeleteFiber}; + + expect(impl_->handle, "create fiber failed"); +} + +Fiber::~Fiber() = default; + +void Fiber::start() +{ + // bookkeeping + parent_fiber_ = *this_fiber(); + *this_fiber() = this; + + // switch to fiber (this will kick-off the job) + ::SwitchToFiber(impl_->handle); + + if (!safe_) + { + // we are no longer suspending if we are here i.e. it is now safe for + // another thread to pick us up + safe_ = true; + } + else + { + // update counter if another fiber was waiting on us + if (counter_ != nullptr) + { + --(*counter_); + } + } +} + +void Fiber::suspend() +{ + *this_fiber() = parent_fiber_; + + ::SwitchToFiber(parent_fiber_->impl_->handle); + + // if we get here then we have been resumed, so rethrow any stored + // exception + if (exception_ != nullptr) + { + std::rethrow_exception(exception_); + } +} + +void Fiber::resume() +{ + // bookkeeping + parent_fiber_ = *this_fiber(); + *this_fiber() = this; + + ::SwitchToFiber(impl_->handle); + + if (!safe_) + { + // we are no longer suspending if we are here i.e. it is now safe for + // another thread to pick us up + safe_ = true; + } + else + { + // update counter if another fiber was waiting on us + if (counter_ != nullptr) + { + --(*counter_); + } + } +} + +bool Fiber::is_safe() const +{ + return safe_; +} + +void Fiber::set_unsafe() +{ + safe_ = false; +} + +bool Fiber::is_being_waited_on() const +{ + return counter_ != nullptr; +} + +std::exception_ptr Fiber::exception() const +{ + return exception_; +} + +void Fiber::thread_to_fiber() +{ + expect(*this_fiber() == nullptr, "thread already a fiber"); + + // thread will clean this up when it ends + auto *fiber = new Fiber{nullptr, nullptr}; + + fiber->impl_->handle = {::ConvertThreadToFiberEx(NULL, FIBER_FLAG_FLOAT_SWITCH), [fiber](auto) { + ::ConvertFiberToThread(); + }}; + + expect(fiber->impl_->handle, "convert thread to fiber failed"); + + *this_fiber() = fiber; +} + +Fiber **Fiber::this_fiber() +{ + // this allows us to get a pointer to the fiber being executed in the + // current thread + thread_local Fiber *this_fiber = nullptr; + return &this_fiber; +} + +} diff --git a/src/jobs/thread/CMakeLists.txt b/src/jobs/thread/CMakeLists.txt new file mode 100644 index 00000000..16c47b40 --- /dev/null +++ b/src/jobs/thread/CMakeLists.txt @@ -0,0 +1,8 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/jobs/thread") + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/thread_job_system.h + ${INCLUDE_ROOT}/thread_job_system_manager.h + thread_job_system.cpp + thread_job_system_manager.cpp) + diff --git a/src/jobs/thread/thread_job_system.cpp b/src/jobs/thread/thread_job_system.cpp new file mode 100644 index 00000000..9f16068c --- /dev/null +++ b/src/jobs/thread/thread_job_system.cpp @@ -0,0 +1,54 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "jobs/thread/thread_job_system.h" + +#include +#include +#include + +#include "jobs/job.h" +#include "log/log.h" + +namespace iris +{ + +ThreadJobSystem::ThreadJobSystem() + : running_(true) +{ +} + +void ThreadJobSystem::add_jobs(const std::vector &jobs) +{ + for (const auto &job : jobs) + { + // we cannot simply call std::async here as the std::future destructor + // will block and wait for the job to finish + // instead we create a shared_ptr for the future and copy it by value + // into the async task + // this ensures it keeps a reference to itself and won't go out of + // scope until the job is complete + auto future = std::make_shared>(); + + *future = std::async(std::launch::async, [future, job] { job(); }); + } +} + +void ThreadJobSystem::wait_for_jobs(const std::vector &jobs) +{ + std::vector> waiting_jobs{}; + + for (const auto &job : jobs) + { + waiting_jobs.emplace_back(std::async(std::launch::async, job)); + } + + for (auto &waiting_job : waiting_jobs) + { + waiting_job.get(); + } +} +} diff --git a/src/jobs/thread/thread_job_system_manager.cpp b/src/jobs/thread/thread_job_system_manager.cpp new file mode 100644 index 00000000..6ecbd6b8 --- /dev/null +++ b/src/jobs/thread/thread_job_system_manager.cpp @@ -0,0 +1,38 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "jobs/thread/thread_job_system_manager.h" + +#include +#include + +#include "core/error_handling.h" +#include "jobs/job.h" +#include "jobs/job_system_manager.h" +#include "jobs/thread/thread_job_system.h" + +namespace iris +{ + +JobSystem *ThreadJobSystemManager::create_job_system() +{ + ensure(job_system_, "job system already created"); + + job_system_ = std::make_unique(); + return job_system_.get(); +} + +void ThreadJobSystemManager::add(const std::vector &jobs) +{ + job_system_->add_jobs(jobs); +} + +void ThreadJobSystemManager::wait(const std::vector &jobs) +{ + job_system_->wait_for_jobs(jobs); +} + +} diff --git a/src/log/CMakeLists.txt b/src/log/CMakeLists.txt new file mode 100644 index 00000000..16c72eb6 --- /dev/null +++ b/src/log/CMakeLists.txt @@ -0,0 +1,16 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/log") + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/basic_formatter.h + ${INCLUDE_ROOT}/colour_formatter.h + ${INCLUDE_ROOT}/emoji_formatter.h + ${INCLUDE_ROOT}/file_outputter.h + ${INCLUDE_ROOT}/log_level.h + ${INCLUDE_ROOT}/log.h + ${INCLUDE_ROOT}/logger.h + ${INCLUDE_ROOT}/stdout_outputter.h + basic_formatter.cpp + colour_formatter.cpp + emoji_formatter.cpp + file_outputter.cpp + stdout_outputter.cpp) diff --git a/src/log/basic_formatter.cpp b/src/log/basic_formatter.cpp new file mode 100644 index 00000000..84dbd44e --- /dev/null +++ b/src/log/basic_formatter.cpp @@ -0,0 +1,79 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "log/basic_formatter.h" + +#include +#include +#include +#include +#include +#include + +#include "log/log_level.h" + +namespace +{ + +/** + * Helper function to extract just the filename from a full path. + * + * @param filename + * Filename to extract from. + * + * @returns + * Filename from supplied string. + */ +std::string format_filename(const std::string &filename) +{ + // find last occurrence of file separator + const auto index = filename.rfind(std::filesystem::path::preferred_separator); + + return std::string{filename.substr(index + 1)}; +} + +/** + * Convert log level to string and get first character. + * + * @param level + * Log level to get first character of. + * + * @returns + * First character of log level. + */ +char first_char_of_level(const iris::LogLevel level) +{ + std::stringstream strm{}; + strm << level; + + const auto str = strm.str(); + + return !str.empty() ? str.front() : 'U'; +} + +} + +namespace iris +{ + +std::string BasicFormatter::format( + const LogLevel level, + const std::string &tag, + const std::string &message, + const std::string &filename, + const int line) +{ + const auto now = std::chrono::system_clock::now(); + const auto seconds = std::chrono::duration_cast(now.time_since_epoch()); + + std::stringstream strm{}; + + strm << first_char_of_level(level) << " " << seconds.count() << " [" << tag << "] " << format_filename(filename) + << ":" << line << " | " << message; + return strm.str(); +} + +} diff --git a/src/log/colour_formatter.cpp b/src/log/colour_formatter.cpp new file mode 100644 index 00000000..c977a659 --- /dev/null +++ b/src/log/colour_formatter.cpp @@ -0,0 +1,46 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "log/colour_formatter.h" + +#include +#include +#include +#include +#include +#include + +#include "log/log_level.h" + +namespace iris +{ + +std::string ColourFormatter::format( + const LogLevel level, + const std::string &tag, + const std::string &message, + const std::string &filename, + const int line) +{ + std::stringstream strm{}; + + // apply an ANSI escape sequence to start colour output + switch (level) + { + case LogLevel::DEBUG: strm << "\x1b[35m"; break; + case LogLevel::INFO: strm << "\x1b[34m"; break; + case LogLevel::WARN: strm << "\x1b[33m"; break; + case LogLevel::ERR: strm << "\x1b[31m"; break; + default: break; + } + + // write message and reset ANSI escape code + strm << formatter_.format(level, tag, message, filename, line) << "\x1b[0m"; + + return strm.str(); +} + +} diff --git a/src/log/emoji_formatter.cpp b/src/log/emoji_formatter.cpp new file mode 100644 index 00000000..4e1978fc --- /dev/null +++ b/src/log/emoji_formatter.cpp @@ -0,0 +1,48 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "log/emoji_formatter.h" + +#include +#include +#include +#include +#include +#include + +#include "log/log_level.h" + +namespace iris +{ + +std::string EmojiFormatter::format( + const LogLevel level, + const std::string &tag, + const std::string &message, + const std::string &filename, + const int line) +{ + std::stringstream strm{}; + + // apply an emoji to start of output + // depending on your text editor the emojis below may not display, but they + // are there! + switch (level) + { + case LogLevel::DEBUG: strm << "🔵 "; break; + case LogLevel::INFO: strm << "ℹ️ "; break; + case LogLevel::WARN: strm << "⚠️ "; break; + case LogLevel::ERR: strm << "❌ "; break; + default: break; + } + + // write message + strm << formatter_.format(level, tag, message, filename, line); + + return strm.str(); +} + +} diff --git a/src/log/file_outputter.cpp b/src/log/file_outputter.cpp new file mode 100644 index 00000000..2b510fdb --- /dev/null +++ b/src/log/file_outputter.cpp @@ -0,0 +1,28 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "log/file_outputter.h" + +#include + +#include "core/error_handling.h" + +namespace iris +{ + +FileOutputter::FileOutputter(const std::string &filename) + : file_(filename, std::ios::out | std::ios::app) +{ + // better check all of these + ensure(file_.is_open() && !file_.bad() && file_.good() && !file_.fail(), "failed to open log file"); +} + +void FileOutputter::output(const std::string &log) +{ + file_ << log << std::endl; +} + +} diff --git a/src/log/stdout_outputter.cpp b/src/log/stdout_outputter.cpp new file mode 100644 index 00000000..b43d3940 --- /dev/null +++ b/src/log/stdout_outputter.cpp @@ -0,0 +1,20 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "log/stdout_outputter.h" + +#include +#include + +namespace iris +{ + +void StdoutFormatter::output(const std::string &log) +{ + std::cout << log << std::endl; +} + +} diff --git a/src/networking/CMakeLists.txt b/src/networking/CMakeLists.txt new file mode 100644 index 00000000..c5640193 --- /dev/null +++ b/src/networking/CMakeLists.txt @@ -0,0 +1,36 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/networking") + +if(IRIS_PLATFORM MATCHES "WIN32") + add_subdirectory("win32") +endif() + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/channel/channel.h + ${INCLUDE_ROOT}/channel/channel_type.h + ${INCLUDE_ROOT}/channel/reliable_ordered_channel.h + ${INCLUDE_ROOT}/channel/unreliable_sequenced_channel.h + ${INCLUDE_ROOT}/channel/unreliable_unordered_channel.h + ${INCLUDE_ROOT}/client_connection_handler.h + ${INCLUDE_ROOT}/data_buffer_deserialiser.h + ${INCLUDE_ROOT}/data_buffer_serialiser.h + ${INCLUDE_ROOT}/networking.h + ${INCLUDE_ROOT}/packet.h + ${INCLUDE_ROOT}/packet_type.h + ${INCLUDE_ROOT}/server_connection_handler.h + ${INCLUDE_ROOT}/server_socket.h + ${INCLUDE_ROOT}/simulated_server_socket.h + ${INCLUDE_ROOT}/simulated_socket.h + ${INCLUDE_ROOT}/socket.h + ${INCLUDE_ROOT}/udp_server_socket.h + ${INCLUDE_ROOT}/udp_socket.h + channel/channel.cpp + channel/reliable_ordered_channel.cpp + channel/unreliable_sequenced_channel.cpp + channel/unreliable_unordered_channel.cpp + client_connection_handler.cpp + packet.cpp + server_connection_handler.cpp + simulated_server_socket.cpp + simulated_socket.cpp + udp_server_socket.cpp + udp_socket.cpp) diff --git a/src/networking/channel/channel.cpp b/src/networking/channel/channel.cpp new file mode 100644 index 00000000..0679f799 --- /dev/null +++ b/src/networking/channel/channel.cpp @@ -0,0 +1,26 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "networking/channel/channel.h" + +namespace iris +{ + +std::vector Channel::yield_send_queue() +{ + std::vector queue; + std::swap(queue, send_queue_); + return queue; +} + +std::vector Channel::yield_receive_queue() +{ + std::vector queue; + std::swap(queue, receive_queue_); + return queue; +} + +} diff --git a/src/networking/channel/reliable_ordered_channel.cpp b/src/networking/channel/reliable_ordered_channel.cpp new file mode 100644 index 00000000..5ee5d936 --- /dev/null +++ b/src/networking/channel/reliable_ordered_channel.cpp @@ -0,0 +1,114 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "networking/channel/reliable_ordered_channel.h" + +#include +#include + +#include "networking/packet.h" + +namespace iris +{ + +ReliableOrderedChannel::ReliableOrderedChannel() + : Channel() + , next_receive_seq_(0u) + , out_sequence_(0u) +{ +} + +void ReliableOrderedChannel::enqueue_send(Packet packet) +{ + // set sequence number of each packet to be once greater than the previous + packet.set_sequence(out_sequence_); + ++out_sequence_; + + send_queue_.emplace_back(std::move(packet)); +} + +void ReliableOrderedChannel::enqueue_receive(Packet packet) +{ + if (packet.type() == PacketType::ACK) + { + // we got an ack so remove the corresponding packet from the send queue + // also use the opportunity to purge acks we've sent from the send queue + send_queue_.erase( + std::remove_if( + std::begin(send_queue_), + std::end(send_queue_), + [&packet](const Packet &p) + { return (p.type() == PacketType::ACK) || (p.sequence() == packet.sequence()); }), + std::end(send_queue_)); + } + else + { + // we got non-ack i.e. something we will want to yield + + // we only care about packets which are the one we are expecting or + // after, anything before will have been yielded + if (packet.sequence() >= next_receive_seq_) + { + // calculate index of packet into our receive queue + const auto index = packet.sequence() - next_receive_seq_; + + // if index is larger than queue then grow the queue + if (index >= receive_queue_.size()) + { + receive_queue_.resize(receive_queue_.size() + index + 1u); + } + + // if this is a new packet i.e. not a duplicate then put it in the + // queue + if (!receive_queue_[index].is_valid()) + { + receive_queue_[index] = packet; + } + } + + // always send an ack, this is because acks aren't reliable so we may + // keep receiving the same packet until an ack finally makes it + // this is why we discard duplicates but still ack + Packet ack{PacketType::ACK, ChannelType::RELIABLE_ORDERED, {}}; + ack.set_sequence(packet.sequence()); + send_queue_.emplace_back(std::move(ack)); + } +} + +std::vector ReliableOrderedChannel::yield_send_queue() +{ + // note that we don't yield the queue but return a copy, this is because + // we want to keep sending packets until we receive an ack + + return send_queue_; +} + +std::vector ReliableOrderedChannel::yield_receive_queue() +{ + // find the first non-valid packet, everything before that will be a + // continuous range of valid packets ready to be yielded + const auto end_of_valid = std::find_if( + std::cbegin(receive_queue_), + std::cend(receive_queue_), + [](const Packet &element) { return !element.is_valid(); }); + + std::vector packets{}; + + if (end_of_valid != std::cbegin(receive_queue_)) + { + // move packets from queue to output collection + packets = std::vector(std::cbegin(receive_queue_), end_of_valid); + receive_queue_.erase(std::begin(receive_queue_), end_of_valid); + + // our next expected sequence number will be one greater than the last + // packet we yield + next_receive_seq_ = packets.back().sequence() + 1u; + } + + return packets; +} + +} diff --git a/src/networking/channel/unreliable_sequenced_channel.cpp b/src/networking/channel/unreliable_sequenced_channel.cpp new file mode 100644 index 00000000..b7df627b --- /dev/null +++ b/src/networking/channel/unreliable_sequenced_channel.cpp @@ -0,0 +1,42 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "networking/channel/unreliable_sequenced_channel.h" +#include + +namespace iris +{ + +UnreliableSequencedChannel::UnreliableSequencedChannel() + : Channel() + , min_sequence_(0u) + , send_sequence_(0u) +{ +} + +void UnreliableSequencedChannel::enqueue_send(Packet packet) +{ + // set sequence number of each packet to be once greater than the previous + packet.set_sequence(send_sequence_); + send_queue_.emplace_back(std::move(packet)); + + ++send_sequence_; +} + +void UnreliableSequencedChannel::enqueue_receive(Packet packet) +{ + // discard all packets that are behind the largest sequence number we've + // seen + if (packet.sequence() >= min_sequence_) + { + receive_queue_.emplace_back(std::move(packet)); + + // by always incrementing here we automatically drop duplicates + min_sequence_ = receive_queue_.back().sequence() + 1u; + } +} + +} diff --git a/src/networking/channel/unreliable_unordered_channel.cpp b/src/networking/channel/unreliable_unordered_channel.cpp new file mode 100644 index 00000000..63432a04 --- /dev/null +++ b/src/networking/channel/unreliable_unordered_channel.cpp @@ -0,0 +1,22 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "networking/channel/unreliable_unordered_channel.h" + +namespace iris +{ + +void UnreliableUnorderedChannel::enqueue_send(Packet packet) +{ + send_queue_.emplace_back(std::move(packet)); +} + +void UnreliableUnorderedChannel::enqueue_receive(Packet packet) +{ + receive_queue_.emplace_back(std::move(packet)); +} + +} diff --git a/src/networking/client_connection_handler.cpp b/src/networking/client_connection_handler.cpp new file mode 100644 index 00000000..78978f17 --- /dev/null +++ b/src/networking/client_connection_handler.cpp @@ -0,0 +1,261 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "networking/client_connection_handler.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/data_buffer.h" +#include "core/error_handling.h" +#include "core/root.h" +#include "jobs/concurrent_queue.h" +#include "jobs/job.h" +#include "jobs/job_system_manager.h" +#include "log/log.h" +#include "networking/channel/channel.h" +#include "networking/channel/reliable_ordered_channel.h" +#include "networking/channel/unreliable_sequenced_channel.h" +#include "networking/channel/unreliable_unordered_channel.h" +#include "networking/data_buffer_deserialiser.h" +#include "networking/data_buffer_serialiser.h" +#include "networking/packet.h" +#include "networking/socket.h" + +namespace +{ + +/** + * Initiate and perform a handshake with the server. + * + * @param socket + * Socket for the connection. + * + * @param channel + * Channel to perform handshake on. + */ +std::uint32_t handshake(iris::Socket *socket, iris::Channel *channel) +{ + auto id = std::numeric_limits::max(); + + // create and enqueue a HELLO packet + static const auto hello = iris::Packet(iris::PacketType::HELLO, iris::ChannelType::RELIABLE_ORDERED, {}); + channel->enqueue_send(hello); + + // send all packets + for (const auto &packet : channel->yield_send_queue()) + { + socket->write(packet.data(), packet.packet_size()); + } + + // keep going until we complete handshake + for (;;) + { + // read a packet + const auto raw_packet = socket->read(sizeof(iris::Packet)); + iris::Packet packet{raw_packet}; + + // enqueue the packet into the channel + channel->enqueue_receive(std::move(packet)); + + // get all received packets + const auto responses = channel->yield_receive_queue(); + + // find the CONNECTED packet + const auto connected = std::find_if( + std::cbegin(responses), + std::cend(responses), + [](const iris::Packet &p) { return p.type() == iris::PacketType::CONNECTED; }); + + // if we got it then get the id from the server and stop looping + if (connected != std::cend(responses)) + { + iris::DataBufferDeserialiser deserialiser{connected->body_buffer()}; + id = deserialiser.pop(); + break; + } + } + + iris::ensure(id == std::numeric_limits::max(), "connection timeout"); + + LOG_ENGINE_INFO("client_connection_handler", "i am: {}", id); + + return id; +} + +/** + * Helper function to handle the start of a sync. + * + * @param channel + * The channel to communicate on. + * + * @param socket + * Socket for the connection. + */ +void handle_sync_start(iris::Channel *channel, iris::Socket *socket) +{ + // serialise our time + const auto now = + std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()); + iris::DataBufferSerialiser serialiser{}; + serialiser.push(static_cast(now.count())); + + // create and enqueue SYNC_RESPONSE + iris::Packet response{iris::PacketType::SYNC_RESPONSE, iris::ChannelType::RELIABLE_ORDERED, serialiser.data()}; + channel->enqueue_send(std::move(response)); + + // send all packets + for (const auto &packet : channel->yield_send_queue()) + { + socket->write(packet.data(), packet.packet_size()); + } +} + +/** + * Helper function to handle sync finish. + * + * @param packet + * SYNC_FINSIH packet. + * + * @returns + * Estimate of lag between client and server. + */ +std::chrono::milliseconds handle_sync_finish(const iris::Packet &packet) +{ + // deserialise times sent from server + iris::DataBufferDeserialiser deserialiser{packet.body_buffer()}; + const auto [client_time_raw, server_time_raw] = deserialiser.pop_tuple(); + const std::chrono::milliseconds client_time(client_time_raw); + const std::chrono::milliseconds server_time(server_time_raw); + + // estimate lag + const auto server_to_client = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch() - server_time); + const auto client_to_server = server_time - client_time; + + return server_to_client + client_to_server; +} + +} + +namespace iris +{ + +ClientConnectionHandler::ClientConnectionHandler(std::unique_ptr socket) + : socket_(std::move(socket)) + , id_(std::numeric_limits::max()) + , lag_(0u) + , channels_() + , queues_() +{ + // setup channels + channels_[ChannelType::UNRELIABLE_UNORDERED] = std::make_unique(); + channels_[ChannelType::UNRELIABLE_SEQUENCED] = std::make_unique(); + channels_[ChannelType::RELIABLE_ORDERED] = std::make_unique(); + + queues_[ChannelType::UNRELIABLE_UNORDERED] = std::make_unique>(); + queues_[ChannelType::UNRELIABLE_SEQUENCED] = std::make_unique>(); + queues_[ChannelType::RELIABLE_ORDERED] = std::make_unique>(); + + id_ = handshake(socket_.get(), channels_[ChannelType::RELIABLE_ORDERED].get()); + + LOG_ENGINE_INFO("client_connection_handler", "connected!"); + + // we want to continually read data as fast as possible, so we do reading in + // a background job + // this will handle any protocol packets and stick data into queues, which + // can then be retrieved by calls to try_read + Root::jobs_manager().add( + {[this]() + { + for (;;) + { + // block and read the next Packet + const auto raw_packet = socket_->read(sizeof(Packet)); + iris::Packet packet{raw_packet}; + + // enqueue the packet into the right channel + const auto channel_type = packet.channel(); + auto *channel = channels_.at(channel_type).get(); + channel->enqueue_receive(std::move(packet)); + + // handle all received packets from that channel + for (const auto &p : channel->yield_receive_queue()) + { + switch (p.type()) + { + case PacketType::DATA: + // we got data, stick it in the queue for this + // channel + queues_[channel_type]->enqueue(p.body_buffer()); + break; + case PacketType::SYNC_START: handle_sync_start(channel, socket_.get()); break; + case PacketType::SYNC_FINISH: lag_ = handle_sync_finish(packet); break; + default: + LOG_ERROR( + "client_connection_handler", "unknown packet type {}", static_cast(p.type())); + break; + } + } + } + }}); +} + +std::optional ClientConnectionHandler::try_read(ChannelType channel_type) +{ + DataBuffer buffer; + + // try and read data from the supplied channel + return queues_[channel_type]->try_dequeue(buffer) ? std::optional{buffer} : std::nullopt; +} + +void ClientConnectionHandler::send(const DataBuffer &data, ChannelType channel_type) +{ + auto *channel = channels_[channel_type].get(); + + // wrap data in a Packet and enqueue + Packet packet{PacketType::DATA, channel_type, data}; + channel->enqueue_send(std::move(packet)); + + // send all packets + for (const auto &p : channel->yield_send_queue()) + { + socket_->write(p.data(), p.packet_size()); + } +} + +void ClientConnectionHandler::flush() +{ + for (auto &[type, channel] : channels_) + { + for (const auto &p : channel->yield_send_queue()) + { + socket_->write(p.data(), p.packet_size()); + } + } +} + +std::uint32_t ClientConnectionHandler::id() const +{ + return id_; +} + +std::chrono::milliseconds ClientConnectionHandler::lag() const +{ + return lag_; +} + +} diff --git a/src/networking/packet.cpp b/src/networking/packet.cpp new file mode 100644 index 00000000..f6d55a35 --- /dev/null +++ b/src/networking/packet.cpp @@ -0,0 +1,162 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "networking/packet.h" + +#include +#include +#include +#include +#include + +#include "core/data_buffer.h" +#include "core/error_handling.h" +#include "networking/channel/channel_type.h" +#include "networking/packet_type.h" + +namespace iris +{ + +Packet::Packet() + : Packet(PacketType::INVAlID, ChannelType::INVAlID, {}) +{ +} + +Packet::Packet(PacketType type, ChannelType channel, const DataBuffer &body) + : header_(type, channel) + , body_() + , size_(body.size()) +{ + expect(body.size() <= sizeof(body_), "body too large"); + + std::memcpy(body_, body.data(), body.size()); +} + +Packet::Packet(const DataBuffer &raw_packet) + : Packet() +{ + const auto size_to_copy = std::min(128ul, raw_packet.size()); + + std::memcpy(this, raw_packet.data(), size_to_copy); + + size_ = size_to_copy - sizeof(Header); +} + +const std::byte *Packet::data() const +{ + return reinterpret_cast(this); +} + +std::byte *Packet::data() +{ + return reinterpret_cast(this); +} + +const std::byte *Packet::body() const +{ + return body_; +} + +std::byte *Packet::body() +{ + return body_; +} + +DataBuffer Packet::body_buffer() const +{ + return DataBuffer(body(), body() + size_); +} + +std::size_t Packet::packet_size() const +{ + // sanity check the Packet class is the expected size + static_assert(sizeof(Packet) == 128 + sizeof(size_), "packet has invalid size"); + + return sizeof(Header) + body_size(); +} + +std::size_t Packet::body_size() const +{ + return size_; +} + +PacketType Packet::type() const +{ + return header_.type; +} + +ChannelType Packet::channel() const +{ + return header_.channel; +} + +bool Packet::is_valid() const +{ + return header_.type != PacketType::INVAlID; +} + +std::uint16_t Packet::sequence() const +{ + return header_.sequence; +} + +void Packet::set_sequence(std::uint16_t sequence) +{ + header_.sequence = sequence; +} + +bool Packet::operator==(const Packet &other) const +{ + return ((packet_size() == other.packet_size()) && (std::memcmp(data(), other.data(), packet_size()) == 0)); +} + +bool Packet::operator!=(const Packet &other) const +{ + return !(*this == other); +} + +std::ostream &operator<<(std::ostream &out, const Packet &packet) +{ + switch (packet.header_.type) + { + case PacketType::INVAlID: out << "INVALID"; break; + case PacketType::HELLO: out << "HELLO"; break; + case PacketType::CONNECTED: out << "CONNECTED"; break; + case PacketType::DATA: out << "DATA"; break; + case PacketType::ACK: out << "ACK"; break; + default: out << "UNKNOWN"; break; + } + + out << ", "; + + switch (packet.header_.channel) + { + case ChannelType::INVAlID: out << "INVALID"; break; + case ChannelType::UNRELIABLE_UNORDERED: out << "UNRELIABLE_UNORDERED"; break; + case ChannelType::UNRELIABLE_SEQUENCED: out << "UNRELIABLE_SEQUENCED"; break; + case ChannelType::RELIABLE_ORDERED: out << "RELIABLE_ORDERED"; break; + default: out << "UNKNOWN"; break; + } + + out << ", "; + + out << "[" << packet.header_.sequence << "]"; + out << " "; + out << packet.size_; + out << " | "; + + out << std::hex; + + for (auto i = 0u; i < std::min(static_cast(packet.size_), 8u); ++i) + { + out << static_cast(packet.body_[i]) << " "; + } + + out << std::dec << std::endl; + + return out; +} +} \ No newline at end of file diff --git a/src/networking/server_connection_handler.cpp b/src/networking/server_connection_handler.cpp new file mode 100644 index 00000000..d996c678 --- /dev/null +++ b/src/networking/server_connection_handler.cpp @@ -0,0 +1,250 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "networking/server_connection_handler.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/data_buffer.h" +#include "core/root.h" +#include "jobs/concurrent_queue.h" +#include "jobs/job.h" +#include "jobs/job_system_manager.h" +#include "log/log.h" +#include "networking/channel/channel_type.h" +#include "networking/channel/reliable_ordered_channel.h" +#include "networking/channel/unreliable_sequenced_channel.h" +#include "networking/channel/unreliable_unordered_channel.h" +#include "networking/data_buffer_deserialiser.h" +#include "networking/data_buffer_serialiser.h" +#include "networking/packet.h" +#include "networking/socket.h" + +namespace +{ + +/** + * Helper function to handle a hello message. This is the first part of the + * handshake and the server needs to respond with CONNECTED. We also use this + * opportunity to start a sync request. + * + * @param id + * Id of connection. + * + * @param channel + * The channel HELLO was received on. + * + * @param socket + * Socket for the connection. + */ +void handle_hello(std::size_t id, iris::Channel *channel, iris::Socket *socket, std::mutex &mutex) +{ + // we will send the client their id + iris::DataBufferSerialiser serialiser{}; + serialiser.push(static_cast(id)); + + // create and enqueue response packets + iris::Packet connected{iris::PacketType::CONNECTED, iris::ChannelType::RELIABLE_ORDERED, serialiser.data()}; + iris::Packet sync_start{iris::PacketType::SYNC_START, iris::ChannelType::RELIABLE_ORDERED, {}}; + + std::vector send_queue{}; + + { + std::unique_lock lock(mutex); + + channel->enqueue_send(std::move(connected)); + channel->enqueue_send(std::move(sync_start)); + send_queue = channel->yield_send_queue(); + } + + // send all packets + for (const auto &packet : send_queue) + { + socket->write(packet.data(), packet.packet_size()); + } +} + +/** + * Helper function to handle the response to a sync. + * + * @param channel + * The channel to communicate on. + * + * @param socket + * Socket for the connection. + * + * @param packet + * The received SYNC_RESPONSE packet. + */ +void handle_sync_response(iris::Channel *channel, iris::Socket *socket, const iris::Packet &packet, std::mutex &mutex) +{ + // get the client time and our time + iris::DataBufferDeserialiser deserialiser{packet.body_buffer()}; + const auto client_time_raw = deserialiser.pop(); + const auto now = + std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()); + + // send the client back their time and out time + iris::DataBufferSerialiser serialiser{}; + serialiser.push(client_time_raw); + serialiser.push(static_cast(now.count())); + iris::Packet sync_finish{iris::PacketType::SYNC_FINISH, iris::ChannelType::RELIABLE_ORDERED, serialiser.data()}; + + std::vector send_queue{}; + + { + std::unique_lock lock(mutex); + channel->enqueue_send(std::move(sync_finish)); + send_queue = channel->yield_send_queue(); + } + + // send all packets + for (const auto &p : send_queue) + { + socket->write(p.data(), p.packet_size()); + } +} + +} + +namespace iris +{ + +/** + * Helper struct to encapsulate data for a connection. + */ +struct ServerConnectionHandler::Connection +{ + Socket *socket; + std::map> channels; + std::chrono::milliseconds rtt; +}; + +ServerConnectionHandler::ServerConnectionHandler( + std::unique_ptr socket, + NewConnectionCallback new_connection_callback, + RecvCallback recv_callback) + : socket_(std::move(socket)) + , new_connection_callback_(new_connection_callback) + , recv_callback_(recv_callback) + , start_(std::chrono::steady_clock::now()) + , connections_() + , mutex_() + , messages_() +{ + // we want to always be accepting connections, so we do this in a background + // job + Root::jobs_manager().add({[this]() + { + for (;;) + { + auto [client_socket, raw_packet, new_connection] = socket_->read(); + + std::hash hash{}; + + const auto id = hash(client_socket); + + if (new_connection) + { + // setup internal struct to manage connection + auto connection = std::make_unique(); + connection->socket = client_socket; + connection->channels[ChannelType::UNRELIABLE_UNORDERED] = + std::make_unique(); + connection->channels[ChannelType::UNRELIABLE_SEQUENCED] = + std::make_unique(); + connection->channels[ChannelType::RELIABLE_ORDERED] = + std::make_unique(); + + connections_[id] = std::move(connection); + } + + auto *connection = connections_[id].get(); + + iris::Packet packet{raw_packet}; + + // enqueue the packet into the right channel + const auto channel_type = packet.channel(); + auto *channel = connection->channels.at(channel_type).get(); + + std::vector receive_queue{}; + + { + std::unique_lock lock(mutex_); + channel->enqueue_receive(std::move(packet)); + receive_queue = channel->yield_receive_queue(); + } + + // handle all received packets from that channel + for (const auto &p : receive_queue) + { + switch (p.type()) + { + case PacketType::HELLO: + { + handle_hello(id, channel, connection->socket, mutex_); + + // we got a new client, fire it back to the + // application + new_connection_callback_(id); + break; + } + case PacketType::DATA: + { + // we got data, fire it back to the application + recv_callback_(id, p.body_buffer(), p.channel()); + break; + } + case PacketType::SYNC_RESPONSE: + { + handle_sync_response(channel, connection->socket, packet, mutex_); + break; + } + default: + LOG_ENGINE_ERROR("server_connection_handler", "unknown packet type"); + } + } + } + }}); +} + +ServerConnectionHandler::~ServerConnectionHandler() = default; + +void ServerConnectionHandler::update() +{ +} + +void ServerConnectionHandler::send(std::size_t id, const DataBuffer &message, ChannelType channel_type) +{ + auto *channel = connections_[id]->channels[channel_type].get(); + auto *socket = connections_[id]->socket; + + { + std::unique_lock lock(mutex_); + + // wrap data in a Packet and enqueue + Packet packet(PacketType::DATA, channel_type, message); + channel->enqueue_send(std::move(packet)); + + // send all packets + for (const auto &p : channel->yield_send_queue()) + { + socket->write(p.data(), p.packet_size()); + } + } +} + +} diff --git a/src/networking/simulated_server_socket.cpp b/src/networking/simulated_server_socket.cpp new file mode 100644 index 00000000..b7ef1640 --- /dev/null +++ b/src/networking/simulated_server_socket.cpp @@ -0,0 +1,43 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "networking/simulated_server_socket.h" + +#include +#include + +#include "networking/server_socket_data.h" +#include "networking/simulated_socket.h" + +namespace iris +{ + +SimulatedServerSocket::SimulatedServerSocket( + std::chrono::milliseconds delay, + std::chrono::milliseconds jitter, + float drop_rate, + ServerSocket *socket) + : socket_(socket) + , client_(nullptr) + , delay_(delay) + , jitter_(jitter) + , drop_rate_(drop_rate) +{ +} + +ServerSocketData SimulatedServerSocket::read() +{ + auto [client_socket, data, new_client] = socket_->read(); + + if (!client_) + { + client_ = std::make_unique(delay_, jitter_, drop_rate_, client_socket); + } + + return {client_.get(), data, new_client}; +} + +} diff --git a/src/networking/simulated_socket.cpp b/src/networking/simulated_socket.cpp new file mode 100644 index 00000000..c0cfd26c --- /dev/null +++ b/src/networking/simulated_socket.cpp @@ -0,0 +1,91 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "networking/simulated_socket.h" + +#include +#include +#include +#include +#include + +#include "core/random.h" +#include "core/root.h" +#include "jobs/concurrent_queue.h" +#include "jobs/job.h" +#include "jobs/job_system_manager.h" +#include "log/log.h" + +using namespace std::chrono_literals; + +namespace iris +{ + +SimulatedSocket::SimulatedSocket( + std::chrono::milliseconds delay, + std::chrono::milliseconds jitter, + float drop_rate, + Socket *socket) + : delay_(delay) + , jitter_(jitter) + , drop_rate_(drop_rate) + , socket_(socket) +{ + // in order to facilitate message delay without blocking we have write() + // enqueue data with a time point, this job then grabs them and can wait + // until the delay has passed before sending + Root::jobs_manager().add({[this]() + { + for (;;) + { + if (write_queue_.empty()) + { + std::this_thread::sleep_for(10ms); + } + else + { + const auto &[buffer, time_point] = write_queue_.dequeue(); + + // wait until its time to send the data + std::this_thread::sleep_until(time_point); + + socket_->write(buffer); + } + } + }}); +} + +SimulatedSocket::~SimulatedSocket() = default; + +std::optional SimulatedSocket::try_read(std::size_t count) +{ + return socket_->try_read(count); +} + +DataBuffer SimulatedSocket::read(std::size_t count) +{ + return socket_->read(count); +} + +void SimulatedSocket::write(const DataBuffer &buffer) +{ + if (!flip_coin(drop_rate_)) + { + const auto jitter = + random_int32(static_cast(-jitter_.count()), static_cast(jitter_.count())); + + // stick the data to be sent on the queue (with the delay time) and + const auto delay = delay_ + std::chrono::milliseconds(jitter); + write_queue_.enqueue(buffer, std::chrono::steady_clock::now() + delay); + } +} + +void SimulatedSocket::write(const std::byte *data, std::size_t size) +{ + write({data, data + size}); +} + +} diff --git a/src/networking/udp_server_socket.cpp b/src/networking/udp_server_socket.cpp new file mode 100644 index 00000000..35b255a6 --- /dev/null +++ b/src/networking/udp_server_socket.cpp @@ -0,0 +1,94 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "networking/udp_server_socket.h" + +#include +#include +#include + +#include "core/auto_release.h" +#include "core/data_buffer.h" +#include "core/error_handling.h" +#include "log/log.h" +#include "networking/networking.h" +#include "networking/server_socket_data.h" +#include "networking/socket.h" +#include "networking/udp_socket.h" + +namespace iris +{ + +UdpServerSocket::UdpServerSocket(const std::string &address, std::uint32_t port) + : connections_() + , socket_() +{ + LOG_ENGINE_INFO("udp_server_socket", "creating server socket ({}:{})", address, port); + + // create socket + socket_ = {::socket(AF_INET, SOCK_DGRAM, 0), CloseSocket}; + ensure(socket_ == INVALID_SOCKET, "socket failed"); + + // configure address + struct sockaddr_in address_storage = {0}; + socklen_t address_length = sizeof(struct sockaddr_in); + std::memset(&address_storage, 0x0, address_length); + + address_storage.sin_family = AF_INET; + inet_pton(AF_INET, address.c_str(), &address_storage.sin_addr.s_addr); + address_storage.sin_port = htons(static_cast(port)); + + // enable multicast + int reuse = 1; + ensure( + ::setsockopt(socket_, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&reuse), sizeof(reuse)) == 0, + "setsockopt failed"); + + // bind socket so we can accept connections + ensure(::bind(socket_, reinterpret_cast(&address_storage), address_length) == 0, "bind failed"); + + LOG_ENGINE_INFO("udp_server_socket", "connected!"); +} + +ServerSocketData UdpServerSocket::read() +{ + struct sockaddr_in address; + socklen_t length = sizeof(address); + + static constexpr auto new_connection_size = 1024u; + DataBuffer buffer(new_connection_size); + + // block and wait for a new connection + const auto read = ::recvfrom( + socket_, + reinterpret_cast(buffer.data()), + static_cast(buffer.size()), + 0, + reinterpret_cast(&address), + &length); + + ensure(read != -1, "recvfrom failed"); + + // resize buffer to amount of data read + buffer.resize(read); + + const auto byte_address = address.sin_addr.s_addr; + + auto new_connection = false; + + if (connections_.count(byte_address) == 0u) + { + connections_[byte_address] = std::make_unique(address, length, socket_.get()); + + new_connection = true; + + LOG_ENGINE_INFO("udp_server_socket", "new connection"); + } + + return {connections_[byte_address].get(), buffer, new_connection}; +} + +} diff --git a/src/networking/udp_socket.cpp b/src/networking/udp_socket.cpp new file mode 100644 index 00000000..376528be --- /dev/null +++ b/src/networking/udp_socket.cpp @@ -0,0 +1,148 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "networking/udp_socket.h" + +#include +#include +#include +#include +#include +#include + +#include "core/data_buffer.h" +#include "core/exception.h" +#include "log/log.h" +#include "networking/networking.h" +#include "networking/socket.h" + +namespace iris +{ + +UdpSocket::UdpSocket(const std::string &address, std::uint16_t port) + : socket_() + , address_() + , address_length_(0) +{ + LOG_ENGINE_INFO("udp_socket", "creating socket ({}:{})", address, port); + + // create socket + socket_ = {::socket(AF_INET, SOCK_DGRAM, 0), CloseSocket}; + if (!socket_) + { + throw Exception("socket failed"); + } + + // configure address + std::memset(&address_, 0x0, sizeof(address_)); + address_length_ = sizeof(address_); + address_.sin_family = AF_INET; + address_.sin_port = htons(port); + + // convert address from text to binary + if (::inet_pton(AF_INET, address.c_str(), &address_.sin_addr.s_addr) != 1) + { + throw Exception("failed to convert ip address"); + } + + // enable socket reuse + int reuse = 1; + if (::setsockopt(socket_, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&reuse), sizeof(reuse)) < 0) + { + throw iris::Exception("setsockopt failed"); + } + + LOG_ENGINE_INFO("udp_socket", "connected!"); +} + +UdpSocket::UdpSocket(struct sockaddr_in socket_address, socklen_t socket_length, SocketHandle socket) + : socket_(socket, nullptr) + , address_(socket_address) + , address_length_(socket_length) +{ +} + +std::optional UdpSocket::try_read(std::size_t count) +{ + std::optional out = DataBuffer(count); + + set_blocking(socket_, false); + + // perform non-blocking read + auto read = ::recvfrom( + socket_, + reinterpret_cast(out->data()), + static_cast(out->size()), + 0, + reinterpret_cast(&address_), + &address_length_); + + if (read == -1) + { + // read failed but not because there was no data + if (!last_call_blocked()) + { + throw iris::Exception("read failed"); + } + + // no data, so reset optional + out.reset(); + } + else + { + // resize buffer to amount of data read + out->resize(read); + } + + return out; +} + +DataBuffer UdpSocket::read(std::size_t count) +{ + DataBuffer buffer(count); + + set_blocking(socket_, true); + + // perform blocking read + auto read = ::recvfrom( + socket_, + reinterpret_cast(buffer.data()), + static_cast(buffer.size()), + 0, + reinterpret_cast(&address_), + &address_length_); + + if (read == -1) + { + throw Exception("recvfrom failed"); + } + + // resize buffer to amount of data read + buffer.resize(read); + + return buffer; +} + +void UdpSocket::write(const DataBuffer &buffer) +{ + write(buffer.data(), buffer.size()); +} + +void UdpSocket::write(const std::byte *data, std::size_t size) +{ + if (::sendto( + socket_, + reinterpret_cast(data), + static_cast(size), + 0, + reinterpret_cast(&address_), + address_length_) != size) + { + throw Exception("sendto failed"); + } +} + +} diff --git a/src/networking/win32/CMakeLists.txt b/src/networking/win32/CMakeLists.txt new file mode 100644 index 00000000..84ed8039 --- /dev/null +++ b/src/networking/win32/CMakeLists.txt @@ -0,0 +1,6 @@ +set(SOURCE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}") +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/networking/windows") + +set(NETWORKING_SRCS + "${INCLUDE_ROOT}/winsock.h" "${SOURCE_ROOT}/winsock.cpp" + PARENT_SCOPE) diff --git a/src/networking/win32/winsock.cpp b/src/networking/win32/winsock.cpp new file mode 100644 index 00000000..ad50ad09 --- /dev/null +++ b/src/networking/win32/winsock.cpp @@ -0,0 +1,32 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "networking/windows/winsock.h" + +#include +#include +#pragma comment(lib, "Ws2_32.lib") + +#include "core/error_handling.h" + +namespace iris +{ +Winsock::Winsock() +{ + WSADATA data = {0}; + + ensure(::WSAStartup(MAKEWORD(2, 2), &data) == 0, "failed to init winsock"); +} + +/** + * Cleanup winsock. + */ +Winsock::~Winsock() +{ + ::WSACleanup(); +} + +} diff --git a/src/physics/CMakeLists.txt b/src/physics/CMakeLists.txt new file mode 100644 index 00000000..b31cc757 --- /dev/null +++ b/src/physics/CMakeLists.txt @@ -0,0 +1,12 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/physics") + +add_subdirectory("bullet") + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/basic_character_controller.h + ${INCLUDE_ROOT}/character_controller.h + ${INCLUDE_ROOT}/collision_shape.h + ${INCLUDE_ROOT}/physics_manager.h + ${INCLUDE_ROOT}/physics_system.h + ${INCLUDE_ROOT}/rigid_body.h + ${INCLUDE_ROOT}/rigid_body_type.h) diff --git a/src/physics/bullet/CMakeLists.txt b/src/physics/bullet/CMakeLists.txt new file mode 100644 index 00000000..f4b2f518 --- /dev/null +++ b/src/physics/bullet/CMakeLists.txt @@ -0,0 +1,18 @@ +set(INCLUDE_ROOT "${PROJECT_SOURCE_DIR}/include/iris/physics/bullet") + +target_sources(iris PRIVATE + ${INCLUDE_ROOT}/bullet_box_collision_shape.h + ${INCLUDE_ROOT}/bullet_capsule_collision_shape.h + ${INCLUDE_ROOT}/bullet_collision_shape.h + ${INCLUDE_ROOT}/bullet_physics_manager.h + ${INCLUDE_ROOT}/bullet_physics_system.h + ${INCLUDE_ROOT}/bullet_rigid_body.h + ${INCLUDE_ROOT}/debug_draw.h + basic_character_controller.cpp + bullet_box_collision_shape.cpp + bullet_capsule_collision_shape.cpp + bullet_physics_manager.cpp + bullet_physics_system.cpp + bullet_rigid_body.cpp + debug_draw.cpp) + diff --git a/src/physics/bullet/basic_character_controller.cpp b/src/physics/bullet/basic_character_controller.cpp new file mode 100644 index 00000000..44a600e8 --- /dev/null +++ b/src/physics/bullet/basic_character_controller.cpp @@ -0,0 +1,132 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "physics/basic_character_controller.h" + +#include + +#include "core/error_handling.h" +#include "core/vector3.h" +#include "log/log.h" +#include "physics/bullet/bullet_rigid_body.h" +#include "physics/collision_shape.h" +#include "physics/physics_system.h" +#include "physics/rigid_body_type.h" + +namespace iris +{ + +BasicCharacterController::BasicCharacterController(PhysicsSystem *physics_system) + : speed_(12.0f) + , mass_(62.0f) + , physics_system_(physics_system) + , body_(nullptr) +{ + // use capsule shape for character + body_ = physics_system_->create_rigid_body( + Vector3{0.0f, 0.0f, 10.0f}, physics_system_->create_capsule_collision_shape(0.5f, 1.7f), RigidBodyType::NORMAL); + + auto *bullet_body = static_cast(body_); + + expect( + bullet_body->type() == RigidBodyType::NORMAL, + "can only create BasicCharacterController with a NORMAL RigidBody"); + + auto *rigid_body = static_cast(bullet_body->handle()); + + // prevent capsule from falling over + rigid_body->setAngularFactor(::btVector3(0.0f, 0.0f, 0.0f)); + + // prevent bullet sleeping the rigid body + rigid_body->setActivationState(DISABLE_DEACTIVATION); +} + +BasicCharacterController::~BasicCharacterController() = default; + +void BasicCharacterController::set_walk_direction(const Vector3 &direction) +{ + const auto current_velocity = body_->linear_velocity(); + const auto velocity = direction * speed_; + + body_->set_linear_velocity({velocity.x, current_velocity.y, velocity.z}); +} + +Vector3 BasicCharacterController::position() const +{ + return body_->position(); +} + +Quaternion BasicCharacterController::orientation() const +{ + return body_->orientation(); +} + +Vector3 BasicCharacterController::linear_velocity() const +{ + return body_->linear_velocity(); +} + +Vector3 BasicCharacterController::angular_velocity() const +{ + return body_->angular_velocity(); +} + +void BasicCharacterController::set_linear_velocity(const Vector3 &linear_velocity) +{ + body_->set_linear_velocity(linear_velocity); +} + +void BasicCharacterController::set_angular_velocity(const Vector3 &angular_velocity) +{ + body_->set_angular_velocity(angular_velocity); +} + +void BasicCharacterController::set_speed(float speed) +{ + speed_ = speed; +} + +void BasicCharacterController::reposition(const Vector3 &position, const Quaternion &orientation) +{ + body_->reposition(position, orientation); +} + +void BasicCharacterController::jump() +{ + // if we are on the ground then jump by applying an upwards impulse + if (on_ground()) + { + body_->apply_impulse({0.0f, mass_ / 10.0f, 0.0f}); + } +} + +bool BasicCharacterController::on_ground() const +{ + auto ground = false; + + // cast a ray downwards to see if what is below us + const auto hit = physics_system_->ray_cast(position(), {0.0f, -1.0f, 0.0f}); + + if (hit) + { + // we are on the ground if the closest object is less than our height + ground = (std::get<1>(*hit) - position()).magnitude() < 1.7f; + } + + return ground; +} + +RigidBody *BasicCharacterController::rigid_body() const +{ + return body_; +} + +void BasicCharacterController::set_collision_shape(CollisionShape *collision_shape) +{ + body_->set_collision_shape(collision_shape); +} + +} diff --git a/src/physics/bullet/bullet_box_collision_shape.cpp b/src/physics/bullet/bullet_box_collision_shape.cpp new file mode 100644 index 00000000..eed94893 --- /dev/null +++ b/src/physics/bullet/bullet_box_collision_shape.cpp @@ -0,0 +1,28 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "physics/bullet/bullet_box_collision_shape.h" + +#include + +#include + +#include "core/vector3.h" + +namespace iris +{ + +BulletBoxCollisionShape::BulletBoxCollisionShape(const Vector3 &half_size) + : shape_(std::make_unique(btVector3{half_size.x, half_size.y, half_size.z})) +{ +} + +btCollisionShape *BulletBoxCollisionShape::handle() const +{ + return shape_.get(); +} + +} diff --git a/src/physics/bullet/bullet_capsule_collision_shape.cpp b/src/physics/bullet/bullet_capsule_collision_shape.cpp new file mode 100644 index 00000000..e2eb3771 --- /dev/null +++ b/src/physics/bullet/bullet_capsule_collision_shape.cpp @@ -0,0 +1,26 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "physics/bullet/bullet_capsule_collision_shape.h" + +#include + +#include + +namespace iris +{ + +BulletCapsuleCollisionShape::BulletCapsuleCollisionShape(float width, float height) + : shape_(std::make_unique(width, height)) +{ +} + +btCollisionShape *BulletCapsuleCollisionShape::handle() const +{ + return shape_.get(); +} + +} diff --git a/src/physics/bullet/bullet_physics_manager.cpp b/src/physics/bullet/bullet_physics_manager.cpp new file mode 100644 index 00000000..d3b7fd63 --- /dev/null +++ b/src/physics/bullet/bullet_physics_manager.cpp @@ -0,0 +1,31 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "physics/bullet/bullet_physics_manager.h" + +#include + +#include "physics/basic_character_controller.h" +#include "physics/bullet/bullet_physics_system.h" +#include "physics/bullet/bullet_rigid_body.h" +#include "physics/physics_manager.h" +#include "physics/rigid_body.h" + +namespace iris +{ + +PhysicsSystem *BulletPhysicsManager::create_physics_system() +{ + physics_system_ = std::make_unique(); + return current_physics_system(); +} + +PhysicsSystem *BulletPhysicsManager::current_physics_system() +{ + return physics_system_.get(); +} + +} diff --git a/src/physics/bullet/bullet_physics_system.cpp b/src/physics/bullet/bullet_physics_system.cpp new file mode 100644 index 00000000..a47f0a07 --- /dev/null +++ b/src/physics/bullet/bullet_physics_system.cpp @@ -0,0 +1,335 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "physics/bullet/bullet_physics_system.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "core/error_handling.h" +#include "core/quaternion.h" +#include "core/vector3.h" +#include "graphics/render_entity.h" +#include "log/log.h" +#include "physics/basic_character_controller.h" +#include "physics/bullet/bullet_box_collision_shape.h" +#include "physics/bullet/bullet_capsule_collision_shape.h" +#include "physics/bullet/bullet_collision_shape.h" +#include "physics/bullet/bullet_rigid_body.h" +#include "physics/bullet/debug_draw.h" +#include "physics/character_controller.h" +#include "physics/rigid_body.h" + +namespace +{ +/** + * Helper function to remove a rigid body from a bullet dynamics world. + * + * @param body + * Body to remove. + * + * @param world + * World to remove from. + */ +void remove_body_from_world(iris::RigidBody *body, btDynamicsWorld *world) +{ + auto *bullet_body = static_cast(body); + + if (body->type() == iris::RigidBodyType::GHOST) + { + auto *bullet_ghost = static_cast<::btGhostObject *>(bullet_body->handle()); + world->removeCollisionObject(bullet_ghost); + } + else + { + auto *bullet_rigid = static_cast<::btRigidBody *>(bullet_body->handle()); + world->removeRigidBody(bullet_rigid); + } +} +} + +namespace iris +{ + +/** + * Saved information about a rigid body. Used in PhysicsState. + */ +struct RigidBodyState +{ + RigidBodyState(const btTransform &transform, const btVector3 &linear_velocity, const btVector3 &angular_velocity) + : transform(transform) + , linear_velocity(linear_velocity) + , angular_velocity(angular_velocity) + { + } + + btTransform transform; + btVector3 linear_velocity; + btVector3 angular_velocity; +}; + +/** + * Struct for saving the state of the physics simulation. It simply stores + * RigidBodyState for all rigid bodies. Note that collision information is + * *not* saved. + */ +struct BulletPhysicsState : public PhysicsState +{ + ~BulletPhysicsState() override = default; + + std::map bodies; +}; + +BulletPhysicsSystem::BulletPhysicsSystem() + : PhysicsSystem() + , broadphase_(nullptr) + , ghost_pair_callback_(nullptr) + , collision_config_(nullptr) + , collision_dispatcher_(nullptr) + , solver_(nullptr) + , world_(nullptr) + , bodies_() + , ignore_() + , character_controllers_() + , debug_draw_(nullptr) + , collision_shapes_() +{ + collision_config_ = std::make_unique<::btDefaultCollisionConfiguration>(); + collision_dispatcher_ = std::make_unique<::btCollisionDispatcher>(collision_config_.get()); + broadphase_ = std::make_unique<::btDbvtBroadphase>(); + solver_ = std::make_unique<::btSequentialImpulseConstraintSolver>(); + world_ = std::make_unique<::btDiscreteDynamicsWorld>( + collision_dispatcher_.get(), broadphase_.get(), solver_.get(), collision_config_.get()); + ghost_pair_callback_ = std::make_unique<::btGhostPairCallback>(); + broadphase_->getOverlappingPairCache()->setInternalGhostPairCallback(ghost_pair_callback_.get()); + + world_->setGravity({0.0f, -10.0f, 0.0f}); + debug_draw_ = nullptr; +} + +BulletPhysicsSystem::~BulletPhysicsSystem() +{ + try + { + for (const auto &body : bodies_) + { + remove_body_from_world(body.get(), world_.get()); + } + + for (const auto &controller : character_controllers_) + { + remove_body_from_world(controller->rigid_body(), world_.get()); + } + } + catch (...) + { + LOG_ERROR("physics_system", "exception caught during dtor"); + } +} + +void BulletPhysicsSystem::step(std::chrono::milliseconds time_step) +{ + const auto ticks = static_cast(time_step.count()); + world_->stepSimulation(ticks / 1000.0f, 1); + + if (debug_draw_) + { + // tell bullet to draw debug world + world_->debugDrawWorld(); + + // now we pass bullet debug information to our render system + debug_draw_->render(); + } +} + +RigidBody *BulletPhysicsSystem::create_rigid_body( + const Vector3 &position, + CollisionShape *collision_shape, + RigidBodyType type) +{ + bodies_.emplace_back( + std::make_unique(position, static_cast(collision_shape), type)); + auto *body = static_cast(bodies_.back().get()); + + if (body->type() == RigidBodyType::GHOST) + { + auto *bullet_ghost = static_cast(body->handle()); + world_->addCollisionObject(bullet_ghost); + } + else + { + auto *bullet_rigid = static_cast(body->handle()); + world_->addRigidBody(bullet_rigid); + } + + return body; +} + +CharacterController *BulletPhysicsSystem::create_character_controller() +{ + character_controllers_.emplace_back(std::make_unique(this)); + return character_controllers_.back().get(); +} + +CollisionShape *BulletPhysicsSystem::create_box_collision_shape(const Vector3 &half_size) +{ + collision_shapes_.emplace_back(std::make_unique(half_size)); + return collision_shapes_.back().get(); +} + +CollisionShape *BulletPhysicsSystem::create_capsule_collision_shape(float width, float height) +{ + collision_shapes_.emplace_back(std::make_unique(width, height)); + return collision_shapes_.back().get(); +} + +void BulletPhysicsSystem::remove(RigidBody *body) +{ + remove_body_from_world(body, world_.get()); + + bodies_.erase( + std::remove_if( + std::begin(bodies_), std::end(bodies_), [body](const auto &element) { return element.get() == body; }), + std::end(bodies_)); +} + +void BulletPhysicsSystem::remove(CharacterController *character) +{ + remove_body_from_world(character->rigid_body(), world_.get()); + + character_controllers_.erase( + std::remove_if( + std::begin(character_controllers_), + std::end(character_controllers_), + [character](const auto &element) { return element.get() == character; }), + std::end(character_controllers_)); +} + +std::optional> BulletPhysicsSystem::ray_cast( + const Vector3 &origin, + const Vector3 &direction) const +{ + std::optional> hit; + + // bullet does ray tracing between two vectors, so we create an end vector + // some great distance away + btVector3 from{origin.x, origin.y, origin.z}; + const auto far_away = origin + (direction * 10000.0f); + btVector3 to{far_away.x, far_away.y, far_away.z}; + + btCollisionWorld::AllHitsRayResultCallback callback{from, to}; + + world_->rayTest(from, to, callback); + + if (callback.hasHit()) + { + auto min = std::numeric_limits::max(); + btVector3 hit_position{}; + const btRigidBody *body = nullptr; + + // find the closest hit object excluding any ignored objects + for (auto i = 0; i < callback.m_collisionObjects.size(); ++i) + { + const auto distance = from.distance(callback.m_hitPointWorld[i]); + if ((distance < min) && (ignore_.count(callback.m_collisionObjects[i]) == 0)) + { + min = distance; + hit_position = callback.m_hitPointWorld[i]; + body = static_cast(callback.m_collisionObjects[i]); + } + } + + if (body != nullptr) + { + hit = { + static_cast(body->getUserPointer()), + {hit_position.x(), hit_position.y(), hit_position.z()}}; + } + } + + return hit; +} + +void BulletPhysicsSystem::ignore_in_raycast(RigidBody *body) +{ + auto *bullet_body = static_cast(body); + ignore_.emplace(bullet_body->handle()); +} + +std::unique_ptr BulletPhysicsSystem::save() +{ + auto state = std::make_unique(); + + // save data for all rigid bodies + for (const auto &body : bodies_) + { + auto *bullet_body = static_cast(body.get()); + auto *bullet_rigid = static_cast(bullet_body->handle()); + + state->bodies.try_emplace( + bullet_rigid, + bullet_rigid->getWorldTransform(), + bullet_rigid->getLinearVelocity(), + bullet_rigid->getAngularVelocity()); + } + + // save data for all character controllers + for (const auto &character : character_controllers_) + { + auto *bullet_body = static_cast(character->rigid_body()); + auto *bullet_rigid = static_cast(bullet_body->handle()); + + state->bodies.try_emplace( + bullet_rigid, + bullet_rigid->getWorldTransform(), + bullet_rigid->getLinearVelocity(), + bullet_rigid->getAngularVelocity()); + } + + return state; +} + +void BulletPhysicsSystem::load(const PhysicsState *state) +{ + const auto *bullet_state = static_cast(state); + + // restore state for each rigid body + for (const auto &[bullet_body, body_state] : bullet_state->bodies) + { + bullet_body->clearForces(); + + bullet_body->setWorldTransform(body_state.transform); + bullet_body->setCenterOfMassTransform(body_state.transform); + bullet_body->setLinearVelocity(body_state.linear_velocity); + bullet_body->setAngularVelocity(body_state.angular_velocity); + } +} + +void BulletPhysicsSystem::enable_debug_draw(RenderEntity *entity) +{ + expect(!debug_draw_, "debug draw already enabled"); + + // create debug drawer, only draw wireframe as that's what we support + debug_draw_ = std::make_unique(entity); + debug_draw_->setDebugMode( + btIDebugDraw::DBG_DrawWireframe | btIDebugDraw::DBG_DrawConstraints | btIDebugDraw::DBG_DrawConstraintLimits); + + world_->setDebugDrawer(debug_draw_.get()); +} + +} diff --git a/src/physics/bullet/bullet_rigid_body.cpp b/src/physics/bullet/bullet_rigid_body.cpp new file mode 100644 index 00000000..fc9b2d4e --- /dev/null +++ b/src/physics/bullet/bullet_rigid_body.cpp @@ -0,0 +1,220 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "physics/bullet/bullet_rigid_body.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "core/exception.h" +#include "core/quaternion.h" +#include "core/vector3.h" +#include "log/log.h" +#include "physics/bullet/bullet_collision_shape.h" +#include "physics/bullet/debug_draw.h" +#include "physics/collision_shape.h" + +namespace iris +{ + +BulletRigidBody::BulletRigidBody(const Vector3 &position, BulletCollisionShape *collision_shape, RigidBodyType type) + : name_() + , type_(type) + , collision_shape_(collision_shape) + , body_(nullptr) + , motion_state_(nullptr) +{ + // convert engine position to bullet form + btTransform start_transform; + start_transform.setIdentity(); + start_transform.setOrigin(btVector3(position.x, position.y, position.z)); + + auto *shape = collision_shape_->handle(); + + // internally we use a derivation of ::btCollisionObject for our rigid + // body, which one depends on the type: + // * GHOST - ::btGhostObject + // * NORMAL/STATIC - ::btRigidBody + + if (type_ == RigidBodyType::GHOST) + { + body_ = std::make_unique(); + body_->setCollisionShape(shape); + body_->setWorldTransform(start_transform); + body_->setCollisionFlags(btCollisionObject::CF_NO_CONTACT_RESPONSE); + } + else + { + // 0 mass means static rigid body + btScalar mass = (type_ == RigidBodyType::STATIC) ? 0.0f : 10.0f; + + btVector3 localInertia(0, 0, 0); + shape->calculateLocalInertia(mass, localInertia); + + motion_state_ = std::make_unique(start_transform); + + btRigidBody::btRigidBodyConstructionInfo rbInfo(mass, motion_state_.get(), shape, localInertia); + + body_ = std::make_unique(rbInfo); + + body_->setFriction(1.0f); + } + + // set the user pointer of the bullet object to our engine object, this + // allows us to get back the RigidBody* when doing collision detection + body_->setUserPointer(this); +} + +Vector3 BulletRigidBody::position() const +{ + const auto transform = body_->getWorldTransform(); + + return {transform.getOrigin().x(), transform.getOrigin().y(), transform.getOrigin().z()}; +} + +Quaternion BulletRigidBody::orientation() const +{ + const auto transform = body_->getWorldTransform(); + const auto orientation = transform.getRotation(); + + return {orientation.x(), orientation.y(), orientation.z(), orientation.w()}; +} + +Vector3 BulletRigidBody::linear_velocity() const +{ + Vector3 velocity{}; + + if (type_ != RigidBodyType::GHOST) + { + const auto bullet_velocity = static_cast(body_.get())->getLinearVelocity(); + + velocity = {bullet_velocity.x(), bullet_velocity.y(), bullet_velocity.z()}; + } + else + { + LOG_ENGINE_WARN("physics", "calling linear_velocity on ghost object"); + } + + return velocity; +} + +Vector3 BulletRigidBody::angular_velocity() const +{ + Vector3 velocity{}; + + if (type_ != RigidBodyType::GHOST) + { + const auto bullet_velocity = static_cast(body_.get())->getAngularVelocity(); + + velocity = {bullet_velocity.x(), bullet_velocity.y(), bullet_velocity.z()}; + } + else + { + LOG_ENGINE_WARN("physics", "calling angular_velocity on ghost object"); + } + + return velocity; +} + +void BulletRigidBody::set_linear_velocity(const Vector3 &linear_velocity) +{ + if (type_ != RigidBodyType::GHOST) + { + btVector3 velocity{linear_velocity.x, linear_velocity.y, linear_velocity.z}; + + static_cast(body_.get())->setLinearVelocity(velocity); + } + else + { + LOG_ENGINE_WARN("physics", "calling set_linear_velocity on ghost object"); + } +} + +void BulletRigidBody::set_angular_velocity(const Vector3 &angular_velocity) +{ + if (type_ != RigidBodyType::GHOST) + { + btVector3 velocity{angular_velocity.x, angular_velocity.y, angular_velocity.z}; + + static_cast(body_.get())->setAngularVelocity(velocity); + } + else + { + LOG_ENGINE_WARN("physics", "calling set_angular_velocity on ghost object"); + } +} + +void BulletRigidBody::reposition(const Vector3 &position, const Quaternion &orientation) +{ + btTransform transform; + transform.setOrigin(btVector3(position.x, position.y, position.z)); + transform.setRotation(btQuaternion(orientation.x, orientation.y, orientation.z, orientation.w)); + + body_->setWorldTransform(transform); + + // also update the motion state for non ghost rigid bodies + if (motion_state_ != nullptr) + { + motion_state_->setWorldTransform(transform); + } +} + +std::string BulletRigidBody::name() const +{ + return name_; +} + +void BulletRigidBody::set_name(const std::string &name) +{ + name_ = name; +} + +RigidBodyType BulletRigidBody::type() const +{ + return type_; +} + +CollisionShape *BulletRigidBody::collision_shape() const +{ + return collision_shape_; +} + +void BulletRigidBody::set_collision_shape(CollisionShape *collision_shape) +{ + collision_shape_ = static_cast(collision_shape); + + body_->setCollisionShape(collision_shape_->handle()); +} + +void BulletRigidBody::apply_impulse(const Vector3 &impulse) +{ + if (type_ != RigidBodyType::GHOST) + { + static_cast(body_.get()) + ->applyImpulse(btVector3{impulse.x, impulse.y, impulse.z}, ::btVector3{}); + } + else + { + LOG_ENGINE_WARN("physics", "calling apply_impulse on ghost object"); + } +} + +btCollisionObject *BulletRigidBody::handle() const +{ + return body_.get(); +} + +} diff --git a/src/physics/bullet/debug_draw.cpp b/src/physics/bullet/debug_draw.cpp new file mode 100644 index 00000000..34814712 --- /dev/null +++ b/src/physics/bullet/debug_draw.cpp @@ -0,0 +1,87 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "physics/bullet/debug_draw.h" + +#include +#include + +#include + +#include "core/colour.h" +#include "core/exception.h" +#include "core/root.h" +#include "core/vector3.h" +#include "graphics/mesh.h" +#include "graphics/vertex_data.h" + +namespace iris +{ + +DebugDraw::DebugDraw(RenderEntity *entity) + : verticies_() + , entity_(entity) + , debug_mode_(0) +{ +} + +void DebugDraw::drawLine(const ::btVector3 &from, const ::btVector3 &to, const ::btVector3 &colour) +{ + verticies_.emplace_back( + Vector3{from.x(), from.y(), from.z()}, + Colour{colour.x(), colour.y(), colour.z()}, + Vector3{to.x(), to.y(), to.z()}, + Colour{colour.x(), colour.y(), colour.z()}); +} + +void DebugDraw::render() +{ + if (!verticies_.empty()) + { + std::vector vertices{}; + std::vector indices; + + for (const auto &[from_position, from_colour, to_position, to_colour] : verticies_) + { + vertices.emplace_back(from_position, Vector3{1.0f}, from_colour, Vector3{}); + indices.emplace_back(static_cast(vertices.size() - 1u)); + + vertices.emplace_back(to_position, Vector3{1.0f}, to_colour, Vector3{}); + indices.emplace_back(static_cast(vertices.size() - 1u)); + } + + entity_->mesh()->update_vertex_data(vertices); + entity_->mesh()->update_index_data(indices); + + verticies_.clear(); + } +} +void DebugDraw::drawContactPoint(const ::btVector3 &, const ::btVector3 &, ::btScalar, int, const ::btVector3 &) +{ + throw Exception("unimplemented"); +} + +void DebugDraw::reportErrorWarning(const char *) +{ + throw Exception("unimplemented"); +} + +void DebugDraw::draw3dText(const ::btVector3 &, const char *) +{ + throw Exception("unimplemented"); +} + +void DebugDraw::setDebugMode(int debugMode) +{ + debug_mode_ = debugMode; +} + +int DebugDraw::getDebugMode() const +{ + return debug_mode_; +} + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 00000000..b18045ce --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,25 @@ + +FetchContent_MakeAvailable(googletest) + +include(GoogleTest) + +mark_as_advanced(BUILD_GMOCK BUILD_GTEST gtest_hide_internal_symbols) + +add_executable(unit_tests "") + +add_subdirectory("core") +add_subdirectory("graphics") +add_subdirectory("jobs") +add_subdirectory("networking") +add_subdirectory("platform") + +target_include_directories(unit_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + +if(IRIS_PLATFORM MATCHES "WIN32") + set_target_properties(unit_tests PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDebug") + set_target_properties(gmock_main PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreadedDebug") +endif() + +target_link_libraries(unit_tests iris gmock_main) +target_compile_definitions(unit_tests PRIVATE IRIS_FORCE_EXPECT) +gtest_discover_tests(unit_tests) diff --git a/tests/core/CMakeLists.txt b/tests/core/CMakeLists.txt new file mode 100644 index 00000000..eb2cf02a --- /dev/null +++ b/tests/core/CMakeLists.txt @@ -0,0 +1,8 @@ +target_sources(unit_tests PRIVATE + auto_release_tests.cpp + colour_tests.cpp + error_handling_tests.cpp + matrix4_tests.cpp + quaternion_tests.cpp + transform_tests.cpp + vector3_tests.cpp) diff --git a/tests/core/auto_release_tests.cpp b/tests/core/auto_release_tests.cpp new file mode 100644 index 00000000..ef5141e4 --- /dev/null +++ b/tests/core/auto_release_tests.cpp @@ -0,0 +1,114 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include + +#include "core/auto_release.h" + +namespace +{ + +using AutoIntPtr = iris::AutoRelease; + +void deleter(int *value) +{ + --*value; +} + +} + +TEST(auto_release, ctor) +{ + int x = 1; + AutoIntPtr v{&x, deleter}; + + ASSERT_EQ(v.get(), &x); + ASSERT_EQ(static_cast(v), &x); + ASSERT_TRUE(v); +} + +TEST(auto_release, function_deleter) +{ + int x = 1; + + { + AutoIntPtr v{&x, deleter}; + } + + ASSERT_EQ(x, 0); +} + +TEST(auto_release, lambda_deleter) +{ + int x = 1; + int y = 2; + + { + AutoIntPtr v{&x, [y](int *p) { *p += y; }}; + } + + ASSERT_EQ(x, 3); +} + +TEST(auto_release, invalid) +{ + AutoIntPtr v{nullptr, deleter}; + + ASSERT_EQ(v.get(), nullptr); + ASSERT_FALSE(v); +} + +TEST(auto_release, move_ctor) +{ + int x = 1; + + { + AutoIntPtr v1{&x, deleter}; + AutoIntPtr v2{std::move(v1)}; + + ASSERT_FALSE(v1); + ASSERT_EQ(v1.get(), nullptr); + ASSERT_TRUE(v2); + ASSERT_EQ(v2.get(), &x); + } + + ASSERT_EQ(x, 0); +} + +TEST(auto_release, move_assignment) +{ + int x = 1; + int y = 1; + + { + AutoIntPtr v1{&x, deleter}; + AutoIntPtr v2{&y, deleter}; + + v2 = std::move(v1); + + ASSERT_FALSE(v1); + ASSERT_EQ(v1.get(), nullptr); + ASSERT_TRUE(v2); + ASSERT_EQ(v2.get(), &x); + } + + ASSERT_EQ(x, 0); + ASSERT_EQ(y, 0); +} + +TEST(auto_release, address) +{ + int x = 1; + + { + AutoIntPtr v{nullptr, deleter}; + + const auto setter = [&x](int **p) { *p = std::addressof(x); }; + setter(&v); + } + + ASSERT_EQ(x, 0); +} diff --git a/tests/core/colour_tests.cpp b/tests/core/colour_tests.cpp new file mode 100644 index 00000000..ad389b1d --- /dev/null +++ b/tests/core/colour_tests.cpp @@ -0,0 +1,123 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include + +#include "core/colour.h" + +TEST(colour, empty_ctor) +{ + const iris::Colour c{}; + + ASSERT_EQ(c.r, 0.0f); + ASSERT_EQ(c.g, 0.0f); + ASSERT_EQ(c.b, 0.0f); + ASSERT_EQ(c.a, 1.0f); +} + +TEST(colour, float_ctor) +{ + const iris::Colour c{0.1f, 0.2f, 0.3f, 0.4f}; + + ASSERT_EQ(c.r, 0.1f); + ASSERT_EQ(c.g, 0.2f); + ASSERT_EQ(c.b, 0.3f); + ASSERT_EQ(c.a, 0.4f); +} + +TEST(colour, byte_ctor) +{ + const iris::Colour c{0x0, 0x7f, 0xff, 0x33}; + + ASSERT_EQ(c, iris::Colour(0.0f, 0.498039216f, 1.0f, 0.2f)); +} + +TEST(colour, byte_ctor_no_alpha) +{ + const iris::Colour c{0x0, 0x7f, 0xff}; + + ASSERT_EQ(c, iris::Colour(0.0f, 0.498039216f, 1.0f, 1.0f)); +} + +TEST(colour, scale) +{ + iris::Colour c1{1.1f, 2.2f, 3.3f}; + auto c2 = c1 * 2.0f; + + ASSERT_EQ(c2, iris::Colour(2.2f, 4.4f, 6.6f, 2.0f)); +} + +TEST(colour, scale_assignment) +{ + iris::Colour c{1.1f, 2.2f, 3.3f}; + c *= 2.0f; + + ASSERT_EQ(c, iris::Colour(2.2f, 4.4f, 6.6f, 2.0f)); +} + +TEST(colour, add) +{ + iris::Colour c1{1.1f, 2.2f, 3.3f}; + auto c2 = c1 + iris::Colour{1.0f, 2.0f, 3.0f}; + + ASSERT_EQ(c2, iris::Colour(2.1f, 4.2f, 6.3f, 2.0f)); +} + +TEST(colour, add_assignment) +{ + iris::Colour c{1.1f, 2.2f, 3.3f}; + c += iris::Colour{1.0f, 2.0f, 3.0f}; + + ASSERT_EQ(c, iris::Colour(2.1f, 4.2f, 6.3f, 2.0f)); +} + +TEST(colour, subtract) +{ + iris::Colour c1{1.1f, 2.2f, 3.3f}; + auto c2 = c1 - iris::Colour{1.0f, 2.0f, 3.0f}; + + ASSERT_EQ(c2, iris::Colour(0.1f, 0.2f, 0.3f, 0.0f)); +} + +TEST(colour, subtract_assignment) +{ + iris::Colour c{1.1f, 2.2f, 3.3f}; + c -= iris::Colour{1.0f, 2.0f, 3.0f}; + + ASSERT_EQ(c, iris::Colour(0.1f, 0.2f, 0.3f, 0.0f)); +} + +TEST(colour, multiply) +{ + iris::Colour c1{1.1f, 2.2f, 3.3f}; + auto c2 = c1 * iris::Colour{1.0f, 2.0f, 3.0f}; + + ASSERT_EQ(c2, iris::Colour(1.1f, 4.4f, 9.9f, 1.0f)); +} + +TEST(colour, multiply_assignment) +{ + iris::Colour c{1.1f, 2.2f, 3.3f}; + c *= iris::Colour{1.0f, 2.0f, 3.0f}; + + ASSERT_EQ(c, iris::Colour(1.1f, 4.4f, 9.9f, 1.0f)); +} + +TEST(colour, equality) +{ + const iris::Colour c1{0.1f, 0.2f, 0.3f, 0.4f}; + const auto c2 = c1; + + ASSERT_EQ(c1, c2); +} + +TEST(colour, inequality) +{ + const iris::Colour c1{0.1f, 0.2f, 0.3f, 0.4f}; + const iris::Colour c2{0.1f, 0.2f, 0.4f, 0.4f}; + + ASSERT_NE(c1, c2); +} diff --git a/tests/core/error_handling_tests.cpp b/tests/core/error_handling_tests.cpp new file mode 100644 index 00000000..cba5daa2 --- /dev/null +++ b/tests/core/error_handling_tests.cpp @@ -0,0 +1,200 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include + +#include +#include + +#include "core/auto_release.h" +#include "core/error_handling.h" + +namespace +{ + +using AutoIntPtr = iris::AutoRelease; + +void deleter(int *value) +{ + --*value; +} + +} + +TEST(expect, bool_expectation_true) +{ + ASSERT_NO_FATAL_FAILURE(iris::expect(true, "")); +} + +TEST(expectDeathTest, bool_expectation_false) +{ + ASSERT_DEATH( + iris::expect(false, "msg"), "msg -> .*error_handling_tests\\.cpp.*"); +} + +TEST(expect, function_expectation_true) +{ + std::function(std::string_view)> func = + [](auto) { return std::optional{}; }; + + ASSERT_NO_FATAL_FAILURE(iris::expect(func, "")); +} + +TEST(expectDeathTest, function_expectation_false) +{ + std::function(std::string_view)> func = + [](auto) { return std::optional{"err: 1"}; }; + + ASSERT_DEATH( + iris::expect(func, "msg"), "err: 1 -> .*error_handling_tests\\.cpp.*"); +} + +TEST(expect, unique_ptr_expectation_true) +{ + auto ptr = std::make_unique(0); + + ASSERT_NO_FATAL_FAILURE(iris::expect(ptr, "")); +} + +TEST(expectDeathTest, unique_ptr_expectation_false) +{ + std::unique_ptr ptr; + + ASSERT_DEATH( + iris::expect(ptr, "msg"), "msg -> .*error_handling_tests\\.cpp.*"); +} + +TEST(expect, auto_release_expectation_true) +{ + int x = 1; + AutoIntPtr auto_release{&x, deleter}; + + ASSERT_NO_FATAL_FAILURE(iris::expect(auto_release, "")); +} + +TEST(expectDeathTest, auto_release_expectation_false) +{ + AutoIntPtr auto_release{}; + + ASSERT_DEATH( + iris::expect(auto_release, "msg"), + "msg -> .*error_handling_tests\\.cpp.*"); +} + +TEST(ensure, bool_expectation_true) +{ + ASSERT_NO_THROW(iris::ensure(true, "")); +} + +TEST(ensure, bool_expectation_false) +{ + auto thrown = false; + try + { + iris::ensure(false, "msg"); + } + catch (iris::Exception &e) + { + ASSERT_THAT( + e.what(), + ::testing::MatchesRegex("msg -> .*error_handling_tests\\.cpp.*")); + thrown = true; + } + + ASSERT_TRUE(thrown); +} + +TEST(ensure, function_expectation_true) +{ + std::function(std::string_view)> func = + [](auto) { return std::optional{}; }; + + ASSERT_NO_THROW(iris::ensure(func, "")); +} + +TEST(ensure, function_expectation_false) +{ + auto thrown = false; + + try + { + std::function(std::string_view)> func = + [](auto) { return std::optional{"err: 1"}; }; + + iris::ensure(func, "msg"); + } + catch (iris::Exception &e) + { + ASSERT_THAT( + e.what(), + ::testing::MatchesRegex( + "err: 1 -> .*error_handling_tests\\.cpp.*")); + thrown = true; + } + + ASSERT_TRUE(thrown); +} + +TEST(ensure, unique_ptr_expectation_true) +{ + auto ptr = std::make_unique(0); + + ASSERT_NO_THROW(iris::ensure(ptr, "")); +} + +TEST(ensure, unique_ptr_expectation_false) +{ + auto thrown = false; + + try + { + std::unique_ptr ptr; + + iris::ensure(ptr, "msg"); + } + catch (iris::Exception &e) + { + ASSERT_THAT( + e.what(), + ::testing::MatchesRegex("msg -> .*error_handling_tests\\.cpp.*")); + thrown = true; + } + + ASSERT_TRUE(thrown); +} + +TEST(ensure, auto_release_expectation_true) +{ + int x = 1; + AutoIntPtr auto_release{&x, deleter}; + + ASSERT_NO_THROW(iris::ensure(auto_release, "")); +} + +TEST(ensure, auto_release_expectation_false) +{ + auto thrown = false; + + try + { + AutoIntPtr auto_release{}; + + iris::ensure(auto_release, "msg"); + } + catch (iris::Exception &e) + { + ASSERT_THAT( + e.what(), + ::testing::MatchesRegex("msg -> .*error_handling_tests\\.cpp.*")); + thrown = true; + } + + ASSERT_TRUE(thrown); +} diff --git a/tests/core/matrix4_tests.cpp b/tests/core/matrix4_tests.cpp new file mode 100644 index 00000000..1239374f --- /dev/null +++ b/tests/core/matrix4_tests.cpp @@ -0,0 +1,588 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include + +#include "core/matrix4.h" +#include "core/quaternion.h" +#include "core/vector3.h" + +TEST(matrix4, constructor) +{ + iris::Matrix4 m{}; + + ASSERT_EQ(m[0], 1.0f); + ASSERT_EQ(m[1], 0.0f); + ASSERT_EQ(m[2], 0.0f); + ASSERT_EQ(m[3], 0.0f); + ASSERT_EQ(m[4], 0.0f); + ASSERT_EQ(m[5], 1.0f); + ASSERT_EQ(m[6], 0.0f); + ASSERT_EQ(m[7], 0.0f); + ASSERT_EQ(m[8], 0.0f); + ASSERT_EQ(m[9], 0.0f); + ASSERT_EQ(m[10], 1.0f); + ASSERT_EQ(m[11], 0.0f); + ASSERT_EQ(m[12], 0.0f); + ASSERT_EQ(m[13], 0.0f); + ASSERT_EQ(m[14], 0.0f); + ASSERT_EQ(m[15], 1.0f); +} + +TEST(matrix4, value_constructor) +{ + iris::Matrix4 m{{{ + 1.0f, + 2.0f, + 3.0f, + 4.0f, + 5.0f, + 6.0f, + 7.0f, + 8.0f, + 9.0f, + 10.0f, + 11.0f, + 12.0f, + 13.0f, + 14.0f, + 15.0f, + 16.0f, + }}}; + + ASSERT_EQ(m[0], 1.0f); + ASSERT_EQ(m[1], 2.0f); + ASSERT_EQ(m[2], 3.0f); + ASSERT_EQ(m[3], 4.0f); + ASSERT_EQ(m[4], 5.0f); + ASSERT_EQ(m[5], 6.0f); + ASSERT_EQ(m[6], 7.0f); + ASSERT_EQ(m[7], 8.0f); + ASSERT_EQ(m[8], 9.0f); + ASSERT_EQ(m[9], 10.0f); + ASSERT_EQ(m[10], 11.0f); + ASSERT_EQ(m[11], 12.0f); + ASSERT_EQ(m[12], 13.0f); + ASSERT_EQ(m[13], 14.0f); + ASSERT_EQ(m[14], 15.0f); + ASSERT_EQ(m[15], 16.0f); +} + +TEST(matrix4, rotation_constructor) +{ + iris::Matrix4 m{iris::Quaternion{1.0f, 2.0f, 3.0f, 4.0f}}; + + iris::Matrix4 expected{{ + -25.0f, + -20.0f, + 22.0f, + 0.0f, + 28.0f, + -19.0f, + 4.0f, + 0.0f, + -10.0f, + 20.0f, + -9.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + }}; + + ASSERT_EQ(m, expected); +} + +TEST(matrix4, rotation_translation_constructor) +{ + iris::Matrix4 m{{1.0f, 2.0f, 3.0f, 4.0f}, {1.0f, 2.0f, 3.0f}}; + + iris::Matrix4 expected{{ + -25.0f, + -20.0f, + 22.0f, + 1.0f, + 28.0f, + -19.0f, + 4.0f, + 2.0f, + -10.0f, + 20.0f, + -9.0f, + 3.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + }}; + + ASSERT_EQ(m, expected); +} + +TEST(matrix4, equality) +{ + iris::Matrix4 m1{{{ + 1.0f, + 2.0f, + 3.0f, + 4.0f, + 5.0f, + 6.0f, + 7.0f, + 8.0f, + 9.0f, + 10.0f, + 11.0f, + 12.0f, + 13.0f, + 14.0f, + 15.0f, + 16.0f, + }}}; + + auto m2 = m1; + + ASSERT_EQ(m1, m2); +} + +TEST(matrix4, inequality) +{ + iris::Matrix4 m1{{{ + 1.0f, + 2.0f, + 3.0f, + 4.0f, + 5.0f, + 6.0f, + 7.0f, + 8.0f, + 9.0f, + 10.0f, + 11.0f, + 12.0f, + 13.0f, + 14.0f, + 15.0f, + 16.0f, + }}}; + + auto m2 = m1; + m2[0] = 100.0f; + + ASSERT_TRUE(m1 != m2); +} + +TEST(matrix4, data) +{ + std::array elements{{ + 1.0f, + 2.0f, + 3.0f, + 4.0f, + 5.0f, + 6.0f, + 7.0f, + 8.0f, + 9.0f, + 10.0f, + 11.0f, + 12.0f, + 13.0f, + 14.0f, + 15.0f, + 16.0f, + }}; + + iris::Matrix4 m{elements}; + + ASSERT_EQ( + std::memcmp(elements.data(), m.data(), sizeof(float) * elements.size()), + 0); +} + +TEST(matrix4, column) +{ + iris::Matrix4 m{{{ + 1.0f, + 2.0f, + 3.0f, + 4.0f, + 5.0f, + 6.0f, + 7.0f, + 8.0f, + 9.0f, + 10.0f, + 11.0f, + 12.0f, + 13.0f, + 14.0f, + 15.0f, + 16.0f, + }}}; + + ASSERT_EQ(m.column(0u), iris::Vector3(1.0f, 5.0f, 9.0f)); + ASSERT_EQ(m.column(1u), iris::Vector3(2.0f, 6.0f, 10.0f)); + ASSERT_EQ(m.column(2u), iris::Vector3(3.0f, 7.0f, 11.0f)); + ASSERT_EQ(m.column(3u), iris::Vector3(4.0f, 8.0f, 12.0f)); +} + +TEST(matrix4, matrix_multiplication) +{ + iris::Matrix4 m1{{{ + 1.0f, + 2.0f, + 3.0f, + 4.0f, + 5.0f, + 6.0f, + 7.0f, + 8.0f, + 9.0f, + 10.0f, + 11.0f, + 12.0f, + 13.0f, + 14.0f, + 15.0f, + 16.0f, + }}}; + + iris::Matrix4 m2 = m1; + auto m3 = m1 * m2; + + iris::Matrix4 expected{ + {{90.0f, + 100.0f, + 110.0f, + 120.0f, + 202.0f, + 228.0f, + 254.0f, + 280.0f, + 314.0f, + 356.0f, + 398.0f, + 440.0f, + 426.0f, + 484.0f, + 542.0f, + 600.0f}}}; + + ASSERT_EQ(m3, expected); +} + +TEST(matrix4, matrix_multiplication_assignment) +{ + iris::Matrix4 m1{{{ + 1.0f, + 2.0f, + 3.0f, + 4.0f, + 5.0f, + 6.0f, + 7.0f, + 8.0f, + 9.0f, + 10.0f, + 11.0f, + 12.0f, + 13.0f, + 14.0f, + 15.0f, + 16.0f, + }}}; + + iris::Matrix4 m2 = m1; + m2 *= m1; + + iris::Matrix4 expected{ + {{90.0f, + 100.0f, + 110.0f, + 120.0f, + 202.0f, + 228.0f, + 254.0f, + 280.0f, + 314.0f, + 356.0f, + 398.0f, + 440.0f, + 426.0f, + 484.0f, + 542.0f, + 600.0f}}}; + + ASSERT_EQ(m2, expected); +} + +TEST(matrix4, vector_multiplication) +{ + iris::Matrix4 m1{{{ + 1.0f, + 2.0f, + 3.0f, + 4.0f, + 5.0f, + 6.0f, + 7.0f, + 8.0f, + 9.0f, + 10.0f, + 11.0f, + 12.0f, + 13.0f, + 14.0f, + 15.0f, + 16.0f, + }}}; + + auto m2 = m1 * iris::Vector3{1.1f, 2.2f, 3.3f}; + + ASSERT_EQ(m2, iris::Vector3(19.4f, 49.8f, 80.2f)); +} + +TEST(matrix4, make_orthographic_projection) +{ + auto m = + iris::Matrix4::make_orthographic_projection(100.0f, 100.0f, 100.0f); + iris::Matrix4 expected{ + {{0.01f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.01f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + -0.01f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f}}}; + + ASSERT_EQ(m, expected); +} + +TEST(matrix4, make_perspective_projection) +{ + auto m = iris::Matrix4::make_perspective_projection( + 0.785398f, 1.0f, 100.0f, 0.1f, 100.0f); + + iris::Matrix4 expected{ + {{241.421402f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 2.414213896f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + -1.002002001f, + -0.2002002001f, + 0.0f, + 0.0f, + -1.0f, + 0.0f}}}; + + ASSERT_EQ(m, expected); +} + +TEST(matrix4, make_look_at) +{ + auto m = + iris::Matrix4::make_look_at({1.0f, 2.0f, 3.0f}, {}, {0.0f, 1.0f, 0.0f}); + + iris::Matrix4 expected{ + {{0.9486833215f, + 0.0f, + -0.3162277937f, + 5.960464478e-08f, + -0.1690308601f, + 0.8451542854f, + -0.5070925355f, + 0.0f, + 0.2672612369f, + 0.5345224738f, + 0.8017836809f, + -3.741657257f, + 0.0f, + 0.0f, + 0.0f, + 1.0f}}}; + + ASSERT_EQ(m, expected); +} + +TEST(matrix4, make_scale) +{ + auto m = iris::Matrix4::make_scale({1.0f, 2.0f, 3.0f}); + + iris::Matrix4 expected{ + {{1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 2.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 3.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f}}}; + + ASSERT_EQ(m, expected); +} + +TEST(matrix4, make_translate) +{ + auto m = iris::Matrix4::make_translate({1.0f, 2.0f, 3.0f}); + + iris::Matrix4 expected{ + {{1.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 1.0f, + 0.0f, + 2.0f, + 0.0f, + 0.0f, + 1.0f, + 3.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f}}}; + + ASSERT_EQ(m, expected); +} + +TEST(matrix4, transpose) +{ + iris::Matrix4 m{{{ + 1.0f, + 2.0f, + 3.0f, + 4.0f, + 5.0f, + 6.0f, + 7.0f, + 8.0f, + 9.0f, + 10.0f, + 11.0f, + 12.0f, + 13.0f, + 14.0f, + 15.0f, + 16.0f, + }}}; + + iris::Matrix4 expected{{{ + 1.0f, + 5.0f, + 9.0f, + 13.0f, + 2.0f, + 6.0f, + 10.0f, + 14.0f, + 3.0f, + 7.0f, + 11.0f, + 15.0f, + 4.0f, + 8.0f, + 12.0f, + 16.0f, + }}}; + + ASSERT_EQ(iris::Matrix4::transpose(m), expected); +} + +TEST(matrix4, invert) +{ + iris::Matrix4 m{{{ + 1.0f, + 1.0f, + 1.0f, + -1.0f, + 1.0f, + 1.0f, + -1.0f, + 1.0f, + 1.0f, + -1.0f, + 1.0f, + 1.0f, + -1.0f, + 1.0f, + 1.0f, + 1.0f, + }}}; + + iris::Matrix4 expected{{{ + 0.25f, + 0.25f, + 0.25f, + -0.25f, + 0.25f, + 0.25f, + -0.25f, + 0.25f, + 0.25f, + -0.25f, + 0.25f, + 0.25f, + -0.25f, + 0.25f, + 0.25f, + 0.25f, + }}}; + + ASSERT_EQ(iris::Matrix4::invert(m), expected); +} + +TEST(matrix4, invert_round_trip) +{ + iris::Matrix4 m{{{ + 1.0f, + 1.0f, + 1.0f, + -1.0f, + 1.0f, + 1.0f, + -1.0f, + 1.0f, + 1.0f, + -1.0f, + 1.0f, + 1.0f, + -1.0f, + 1.0f, + 1.0f, + 1.0f, + }}}; + + ASSERT_EQ(m * iris::Matrix4::invert(m), iris::Matrix4{}); +} diff --git a/tests/core/quaternion_tests.cpp b/tests/core/quaternion_tests.cpp new file mode 100644 index 00000000..81108ce8 --- /dev/null +++ b/tests/core/quaternion_tests.cpp @@ -0,0 +1,173 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include + +#include "core/quaternion.h" +#include "core/vector3.h" + +TEST(quaternion, basic_constructor) +{ + iris::Quaternion q{}; + ASSERT_EQ(q, iris::Quaternion(0.0f, 0.0f, 0.0f, 1.0f)); +} + +TEST(quaternion, axis_angle_constructor) +{ + iris::Quaternion q{{1.0f, 0.0f, 0.0f}, 0.5f}; + ASSERT_EQ(q, iris::Quaternion(0.2474039644f, 0.0f, 0.0f, 0.9689124227f)); +} + +TEST(quaternion, compontent_constructor) +{ + iris::Quaternion q{1.0f, 2.0f, 3.0f, 4.0f}; + ASSERT_EQ(q, iris::Quaternion(1.0f, 2.0f, 3.0f, 4.0f)); +} + +TEST(quaternion, euler_constructor) +{ + iris::Quaternion q{0.1f, 0.2f, 0.3f}; + std::cout << std::setprecision(10) << q << std::endl; + ASSERT_EQ( + q, + iris::Quaternion( + 0.1435721964f, 0.1060205176f, 0.0342707932f, 0.9833474755f)); +} + +TEST(quaternion, multiply) +{ + iris::Quaternion q1{{1.0f, 0.0f, 0.0f}, 0.5f}; + iris::Quaternion q2{{0.0f, 0.0f, 1.0f}, 0.2f}; + auto q3 = q1 * q2; + + ASSERT_EQ( + q3, + iris::Quaternion( + 0.2461679727f, -0.02469918504f, 0.09672984481f, 0.9640719295f)); +} + +TEST(quaternion, multiply_assignment) +{ + iris::Quaternion q1{{1.0f, 0.0f, 0.0f}, 0.5f}; + iris::Quaternion q2{{0.0f, 0.0f, 1.0f}, 0.2f}; + q1 *= q2; + + ASSERT_EQ( + q1, + iris::Quaternion( + 0.2461679727f, -0.02469918504f, 0.09672984481f, 0.9640719295f)); +} + +TEST(quaternion, vector_addition) +{ + iris::Quaternion q1{{1.0f, 0.0f, 0.0f}, 0.5f}; + iris::Vector3 v{0.0f, 0.0f, 1.0f}; + auto q2 = q1 + v; + + ASSERT_EQ( + q2, + iris::Quaternion( + 0.2474039644f, 0.1237019822f, 0.4844562113f, 0.9689124227f)); +} + +TEST(quaternion, vector_addition_assignment) +{ + iris::Quaternion q{{1.0f, 0.0f, 0.0f}, 0.5f}; + iris::Vector3 v{0.0f, 0.0f, 1.0f}; + q += v; + + ASSERT_EQ( + q, + iris::Quaternion( + 0.2474039644f, 0.1237019822f, 0.4844562113f, 0.9689124227f)); +} + +TEST(quaternion, scale) +{ + const iris::Quaternion q1{1.0f, 2.0f, 3.0f, 4.0f}; + const auto q2 = q1 * 1.5f; + + ASSERT_EQ(q2, iris::Quaternion(1.5f, 3.0f, 4.5f, 6.0f)); +} + +TEST(quaternion, scale_assignment) +{ + iris::Quaternion q{1.0f, 2.0f, 3.0f, 4.0f}; + q *= 1.5f; + + ASSERT_EQ(q, iris::Quaternion(1.5f, 3.0f, 4.5f, 6.0f)); +} + +TEST(quaternion, subtraction) +{ + const iris::Quaternion q1(1.0f, 2.0f, 3.0f, 4.0f); + const auto q2 = q1 - iris::Quaternion{0.1f, 0.2, 0.3f, 0.4f}; + + ASSERT_EQ(q2, iris::Quaternion(0.9f, 1.8f, 2.7f, 3.6f)); +} + +TEST(quaternion, subtraction_assignment) +{ + iris::Quaternion q(1.0f, 2.0f, 3.0f, 4.0f); + q -= iris::Quaternion{0.1f, 0.2, 0.3f, 0.4f}; + + ASSERT_EQ(q, iris::Quaternion(0.9f, 1.8f, 2.7f, 3.6f)); +} + +TEST(quaternion, addition) +{ + const iris::Quaternion q1(1.0f, 2.0f, 3.0f, 4.0f); + const auto q2 = q1 + iris::Quaternion{0.1f, 0.2, 0.3f, 0.4f}; + + ASSERT_EQ(q2, iris::Quaternion(1.1f, 2.2f, 3.3f, 4.4f)); +} + +TEST(quaternion, addition_assignment) +{ + iris::Quaternion q(1.0f, 2.0f, 3.0f, 4.0f); + q += iris::Quaternion{0.1f, 0.2, 0.3f, 0.4f}; + + ASSERT_EQ(q, iris::Quaternion(1.1f, 2.2f, 3.3f, 4.4f)); +} + +TEST(quaternion, negate) +{ + iris::Quaternion q(1.0f, 2.0f, 3.0f, 4.0f); + + ASSERT_EQ(-q, iris::Quaternion(-1.0f, -2.0f, -3.0f, -4.0f)); +} + +TEST(quaternion, dot) +{ + const iris::Quaternion q1(1.1f, 2.2f, 1.1f, 2.2f); + const iris::Quaternion q2(0.1f, 0.2f, 0.3f, 0.4f); + + ASSERT_EQ(q1.dot(q2), 1.76f); +} + +TEST(quaternion, slerp) +{ + iris::Quaternion q1(1.1f, 2.2f, 1.1f, 2.2f); + const iris::Quaternion q2(0.1f, 0.2f, 0.3f, 0.4f); + + q1.slerp(q2, 0.5f); + + ASSERT_EQ( + q1, + iris::Quaternion( + 0.3007528484f, 0.6015056968f, 0.3508783281f, 0.6516311169f)); +} + +TEST(quaternion, normalise) +{ + iris::Quaternion q{1.0f, 2.0f, 3.0f, 4.0f}; + q.normalise(); + + ASSERT_EQ( + q, + iris::Quaternion( + 0.1825741827f, 0.3651483655f, 0.5477225184f, 0.730296731f)); +} diff --git a/tests/core/transform_tests.cpp b/tests/core/transform_tests.cpp new file mode 100644 index 00000000..2881f388 --- /dev/null +++ b/tests/core/transform_tests.cpp @@ -0,0 +1,110 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include + +#include "core/matrix4.h" +#include "core/quaternion.h" +#include "core/transform.h" +#include "core/vector3.h" + +TEST(transform, empty_ctor) +{ + const iris::Transform transform{}; + + const auto expected = iris::Matrix4::make_translate({}) * + iris::Matrix4(iris::Quaternion{}) * + iris::Matrix4::make_scale({1.0f}); + + EXPECT_EQ(transform.matrix(), expected); +} + +TEST(transform, transform_ctor) +{ + const iris::Vector3 translate{1.0f, 2.0f, 3.0f}; + const iris::Quaternion rotation{1.0f, 2.0f, 3.0f, 4.0f}; + const iris::Vector3 scale{2.0f}; + + const auto expected = iris::Matrix4::make_translate(translate) * + iris::Matrix4(rotation) * + iris::Matrix4::make_scale(scale); + const iris::Transform transform{translate, rotation, scale}; + + EXPECT_EQ(transform.matrix(), expected); +} + +TEST(transform, transform) +{ + const iris::Vector3 translate1{1.0f, 2.0f, 3.0f}; + const iris::Vector3 translate2{1.1f, 2.2f, 3.3f}; + const iris::Quaternion rotation{1.0f, 2.0f, 3.0f, 4.0f}; + const iris::Vector3 scale{2.0f}; + + iris::Transform transform{translate1, rotation, scale}; + const auto get_translate1 = transform.translation(); + transform.set_translation(translate2); + const auto get_translate2 = transform.translation(); + + ASSERT_EQ(get_translate1, translate1); + ASSERT_EQ(get_translate2, translate2); +} + +TEST(transform, rotation) +{ + const iris::Vector3 translate{1.0f, 2.0f, 3.0f}; + const iris::Quaternion rotation1{1.0f, 2.0f, 3.0f, 4.0f}; + const iris::Quaternion rotation2{1.1f, 2.2f, 3.3f, 4.4f}; + const iris::Vector3 scale{2.0f}; + + iris::Transform transform{translate, rotation1, scale}; + const auto get_rotation1 = transform.rotation(); + transform.set_rotation(rotation2); + const auto get_rotation2 = transform.rotation(); + + ASSERT_EQ(get_rotation1, rotation1); + ASSERT_EQ(get_rotation2, rotation2); +} + +TEST(transform, scale) +{ + const iris::Vector3 translate{1.0f, 2.0f, 3.0f}; + const iris::Quaternion rotation{1.0f, 2.0f, 3.0f, 4.0f}; + const iris::Vector3 scale1{2.0f}; + const iris::Vector3 scale2{2.2f}; + + iris::Transform transform{translate, rotation, scale1}; + const auto get_scale1 = transform.scale(); + transform.set_scale(scale2); + const auto get_scale2 = transform.scale(); + + ASSERT_EQ(get_scale1, scale1); + ASSERT_EQ(get_scale2, scale2); +} + +TEST(transform, equality) +{ + const iris::Vector3 translate{1.0f, 2.0f, 3.0f}; + const iris::Quaternion rotation{1.0f, 2.0f, 3.0f, 4.0f}; + const iris::Vector3 scale{2.0f}; + + const iris::Transform transform1{translate, rotation, scale}; + const iris::Transform transform2{translate, rotation, scale}; + + ASSERT_EQ(transform1, transform2); +} + +TEST(transform, inequality) +{ + const iris::Vector3 translate1{1.0f, 2.0f, 3.0f}; + const iris::Vector3 translate2{2.0f, 2.0f, 3.0f}; + const iris::Quaternion rotation{1.0f, 2.0f, 3.0f, 4.0f}; + const iris::Vector3 scale{2.0f}; + + const iris::Transform transform1{translate1, rotation, scale}; + const iris::Transform transform2{translate2, rotation, scale}; + + ASSERT_NE(translate1, translate2); +} diff --git a/tests/core/vector3_tests.cpp b/tests/core/vector3_tests.cpp new file mode 100644 index 00000000..fcc0ec8d --- /dev/null +++ b/tests/core/vector3_tests.cpp @@ -0,0 +1,194 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include + +#include "core/vector3.h" + +TEST(vector3, default_constructor) +{ + iris::Vector3 v{}; + + ASSERT_EQ(v.x, 0.0f); + ASSERT_EQ(v.y, 0.0f); + ASSERT_EQ(v.x, 0.0f); +} + +TEST(vector3, single_value_constructor) +{ + iris::Vector3 v{1.1f}; + + ASSERT_EQ(v.x, 1.1f); + ASSERT_EQ(v.y, 1.1f); + ASSERT_EQ(v.z, 1.1f); +} + +TEST(vector3, multi_value_constructor) +{ + iris::Vector3 v{1.1f, 2.2f, 3.3f}; + + ASSERT_EQ(v.x, 1.1f); + ASSERT_EQ(v.y, 2.2f); + ASSERT_EQ(v.z, 3.3f); +} + +TEST(vector3, equality) +{ + iris::Vector3 v1{1.0f, 2.0f, 3.0f}; + iris::Vector3 v2{1.0f, 2.0f, 3.0f}; + + ASSERT_EQ(v1, v2); +} + +TEST(vector3, inequality) +{ + iris::Vector3 v1{1.0f, 2.0f, 3.0f}; + iris::Vector3 v2{1.1f, 2.2f, 3.2f}; + + ASSERT_NE(v1, v2); +} + +TEST(vector3, scale) +{ + iris::Vector3 v1{1.1f, 2.2f, 3.3f}; + auto v2 = v1 * 2.0f; + + ASSERT_EQ(v2, iris::Vector3(2.2f, 4.4f, 6.6f)); +} + +TEST(vector3, scale_assignment) +{ + iris::Vector3 v{1.1f, 2.2f, 3.3f}; + v *= 2.0f; + + ASSERT_EQ(v, iris::Vector3(2.2f, 4.4f, 6.6f)); +} + +TEST(vector3, add) +{ + iris::Vector3 v1{1.1f, 2.2f, 3.3f}; + auto v2 = v1 + iris::Vector3{1.0f, 2.0f, 3.0f}; + + ASSERT_EQ(v2, iris::Vector3(2.1f, 4.2f, 6.3f)); +} + +TEST(vector3, add_assignment) +{ + iris::Vector3 v{1.1f, 2.2f, 3.3f}; + v += iris::Vector3{1.0f, 2.0f, 3.0f}; + + ASSERT_EQ(v, iris::Vector3(2.1f, 4.2f, 6.3f)); +} + +TEST(vector3, subtract) +{ + iris::Vector3 v1{1.1f, 2.2f, 3.3f}; + auto v2 = v1 - iris::Vector3{1.0f, 2.0f, 3.0f}; + + ASSERT_EQ(v2, iris::Vector3(0.1f, 0.2f, 0.3f)); +} + +TEST(vector3, subtract_assignment) +{ + iris::Vector3 v{1.1f, 2.2f, 3.3f}; + v -= iris::Vector3{1.0f, 2.0f, 3.0f}; + + ASSERT_EQ(v, iris::Vector3(0.1f, 0.2f, 0.3f)); +} + +TEST(vector3, multiply) +{ + iris::Vector3 v1{1.1f, 2.2f, 3.3f}; + auto v2 = v1 * iris::Vector3{1.0f, 2.0f, 3.0f}; + + ASSERT_EQ(v2, iris::Vector3(1.1f, 4.4f, 9.9f)); +} + +TEST(vector3, multiply_assignment) +{ + iris::Vector3 v{1.1f, 2.2f, 3.3f}; + v *= iris::Vector3{1.0f, 2.0f, 3.0f}; + + ASSERT_EQ(v, iris::Vector3(1.1f, 4.4f, 9.9f)); +} + +TEST(vector3, negate) +{ + iris::Vector3 v{1.1f, 2.2f, 3.3f}; + + ASSERT_EQ(-v, iris::Vector3(-1.1f, -2.2f, -3.3f)); +} + +TEST(vector3, dot) +{ + ASSERT_EQ( + iris::Vector3(1.0f, 3.0f, -5.0f).dot(iris::Vector3(4.0f, -2.0f, -1.0f)), + 3.0f); +} + +TEST(vector3, cross) +{ + ASSERT_EQ( + iris::Vector3(2.0f, 3.0f, 4.0f).cross(iris::Vector3(5.0f, 6.0f, 7.0f)), + iris::Vector3(-3.0f, 6.0f, -3.0f)); +} + +TEST(vector3, cross_static) +{ + ASSERT_EQ( + iris::Vector3::cross({2.0f, 3.0f, 4.0f}, {5.0f, 6.0f, 7.0f}), + iris::Vector3(-3.0f, 6.0f, -3.0f)); +} + +TEST(vector3, normalise) +{ + iris::Vector3 v{1.0f, 2.0f, 3.0f}; + v.normalise(); + + ASSERT_EQ(v, iris::Vector3(0.2672612f, 0.5345225f, 0.8017837f)); +} + +TEST(vector3, normalise_zero_vector3) +{ + iris::Vector3 v{}; + v.normalise(); + + ASSERT_EQ(v, iris::Vector3()); +} + +TEST(vector3, normalise_static) +{ + ASSERT_EQ( + iris::Vector3::normalise(iris::Vector3(1.0f, 2.0f, 3.0f)), + iris::Vector3(0.2672612f, 0.5345225f, 0.8017837f)); +} + +TEST(vector3, magnitude) +{ + iris::Vector3 v{1.0f, 2.0f, 3.0f}; + + ASSERT_FLOAT_EQ(v.magnitude(), 3.7416574954986572265625f); +} + +TEST(vector3, lerp) +{ + iris::Vector3 vec(0.0f, 0.0f, 0.f); + iris::Vector3 end(1.0f, 1.0f, 1.0f); + + vec.lerp(end, 0.5f); + + ASSERT_EQ(vec, iris::Vector3(0.5f, 0.5f, 0.5f)); +} + +TEST(vector3, lerp_static) +{ + iris::Vector3 start(0.0f, 0.0f, 0.f); + iris::Vector3 end(1.0f, 1.0f, 1.0f); + + const auto result = iris::Vector3::lerp(start, end, 0.5f); + + ASSERT_EQ(result, iris::Vector3(0.5f, 0.5f, 0.5f)); +} diff --git a/tests/fakes/fake_light.h b/tests/fakes/fake_light.h new file mode 100644 index 00000000..c2c0b26f --- /dev/null +++ b/tests/fakes/fake_light.h @@ -0,0 +1,40 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "graphics/lights/light.h" +#include "graphics/lights/light_type.h" + +class FakeLight : public iris::Light +{ + public: + ~FakeLight() override = default; + + iris::LightType type() const override + { + return static_cast( + std::numeric_limits::max()); + } + + std::array colour_data() const override + { + return {}; + } + + std::array world_space_data() const override + { + return {}; + } + + std::array attenuation_data() const override + { + return {}; + } +}; diff --git a/tests/fakes/fake_material.h b/tests/fakes/fake_material.h new file mode 100644 index 00000000..525947af --- /dev/null +++ b/tests/fakes/fake_material.h @@ -0,0 +1,23 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include + +#include "graphics/material.h" +#include "graphics/texture.h" + +class FakeMaterial : public iris::Material +{ + public: + ~FakeMaterial() override = default; + + std::vector textures() const override + { + return {}; + } +}; diff --git a/tests/fakes/fake_render_target.h b/tests/fakes/fake_render_target.h new file mode 100644 index 00000000..25888985 --- /dev/null +++ b/tests/fakes/fake_render_target.h @@ -0,0 +1,28 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "graphics/render_target.h" +#include "graphics/texture.h" + +#include "fakes/fake_texture.h" + +class FakeRenderTarget : public iris::RenderTarget +{ + public: + FakeRenderTarget() + : iris::RenderTarget( + std::make_unique(), + std::make_unique()) + { + } + + ~FakeRenderTarget() override = default; +}; diff --git a/tests/fakes/fake_renderer.h b/tests/fakes/fake_renderer.h new file mode 100644 index 00000000..4ef3268a --- /dev/null +++ b/tests/fakes/fake_renderer.h @@ -0,0 +1,81 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include + +#include "graphics/render_command.h" +#include "graphics/render_command_type.h" +#include "graphics/render_pass.h" +#include "graphics/render_target.h" +#include "graphics/renderer.h" + +class FakeRenderer : public iris::Renderer +{ + public: + FakeRenderer(const std::vector &render_queue) + : iris::Renderer() + { + render_queue_ = render_queue; + } + + std::vector call_log() const + { + return call_log_; + } + + ~FakeRenderer() override = default; + + // overridden methods which just log when they are called + + void set_render_passes(const std::vector &) override + { + } + + iris::RenderTarget *create_render_target(std::uint32_t, std::uint32_t) + override + { + return nullptr; + } + + void pre_render() override + { + } + + void execute_upload_texture(iris::RenderCommand &) override + { + call_log_.emplace_back(iris::RenderCommandType::UPLOAD_TEXTURE); + } + + void execute_pass_start(iris::RenderCommand &) override + { + call_log_.emplace_back(iris::RenderCommandType::PASS_START); + } + + void execute_draw(iris::RenderCommand &) override + { + call_log_.emplace_back(iris::RenderCommandType::DRAW); + } + + void execute_pass_end(iris::RenderCommand &) override + { + call_log_.emplace_back(iris::RenderCommandType::PASS_END); + } + + void execute_present(iris::RenderCommand &) override + { + call_log_.emplace_back(iris::RenderCommandType::PRESENT); + } + + void post_render() override + { + } + + private: + std::vector call_log_; +}; \ No newline at end of file diff --git a/tests/fakes/fake_texture.h b/tests/fakes/fake_texture.h new file mode 100644 index 00000000..e5ef5f57 --- /dev/null +++ b/tests/fakes/fake_texture.h @@ -0,0 +1,21 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "graphics/texture.h" +#include "graphics/texture_usage.h" + +class FakeTexture : public iris::Texture +{ + public: + FakeTexture() + : iris::Texture({}, 1u, 1u, iris::TextureUsage::IMAGE) + { + } + + ~FakeTexture() override = default; +}; diff --git a/tests/graphics/CMakeLists.txt b/tests/graphics/CMakeLists.txt new file mode 100644 index 00000000..cdf831d7 --- /dev/null +++ b/tests/graphics/CMakeLists.txt @@ -0,0 +1,4 @@ +target_sources(unit_tests PRIVATE + render_command_tests.cpp + render_queue_builder_tests.cpp + renderer_tests.cpp) diff --git a/tests/graphics/render_command_tests.cpp b/tests/graphics/render_command_tests.cpp new file mode 100644 index 00000000..4e3fa04e --- /dev/null +++ b/tests/graphics/render_command_tests.cpp @@ -0,0 +1,144 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include + +#include "core/vector3.h" +#include "graphics/render_command.h" +#include "graphics/render_command_type.h" +#include "graphics/render_pass.h" + +#include "fakes/fake_light.h" +#include "fakes/fake_material.h" +#include "fakes/fake_render_target.h" + +TEST(render_command_tests, default_ctor) +{ + iris::RenderCommand cmd{}; + + ASSERT_EQ(cmd.type(), iris::RenderCommandType::PASS_START); + ASSERT_EQ(cmd.render_pass(), nullptr); + ASSERT_EQ(cmd.material(), nullptr); + ASSERT_EQ(cmd.render_entity(), nullptr); + ASSERT_EQ(cmd.shadow_map(), nullptr); + ASSERT_EQ(cmd.light(), nullptr); +} + +TEST(render_command_tests, ctor) +{ + const auto type = iris::RenderCommandType::DRAW; + const iris::RenderPass render_pass{nullptr, nullptr, nullptr}; + const FakeMaterial material{}; + const iris::RenderEntity render_entity{nullptr, iris::Vector3{}}; + const FakeRenderTarget shadow_map{}; + const FakeLight light{}; + + const iris::RenderCommand cmd{ + type, &render_pass, &material, &render_entity, &shadow_map, &light}; + + ASSERT_EQ(cmd.type(), type); + ASSERT_EQ(cmd.render_pass(), &render_pass); + ASSERT_EQ(cmd.material(), &material); + ASSERT_EQ(cmd.render_entity(), &render_entity); + ASSERT_EQ(cmd.shadow_map(), &shadow_map); + ASSERT_EQ(cmd.light(), &light); +} + +TEST(render_command_tests, get_set_type) +{ + iris::RenderCommand cmd{}; + const auto new_value = iris::RenderCommandType::DRAW; + + cmd.set_type(new_value); + + ASSERT_EQ(cmd.type(), new_value); +} + +TEST(render_command_tests, get_set_render_pass) +{ + iris::RenderCommand cmd{}; + const iris::RenderPass new_value{nullptr, nullptr, nullptr}; + + cmd.set_render_pass(&new_value); + + ASSERT_EQ(cmd.render_pass(), &new_value); +} + +TEST(render_command_tests, get_set_material) +{ + iris::RenderCommand cmd{}; + const FakeMaterial new_value{}; + + cmd.set_material(&new_value); + + ASSERT_EQ(cmd.material(), &new_value); +} + +TEST(render_command_tests, get_set_render_entity) +{ + iris::RenderCommand cmd{}; + const iris::RenderEntity new_value{nullptr, iris::Vector3{}}; + + cmd.set_render_entity(&new_value); + + ASSERT_EQ(cmd.render_entity(), &new_value); +} + +TEST(render_command_tests, get_set_shadow_map) +{ + iris::RenderCommand cmd{}; + const FakeRenderTarget new_value{}; + + cmd.set_shadow_map(&new_value); + + ASSERT_EQ(cmd.shadow_map(), &new_value); +} + +TEST(render_command_tests, get_set_light) +{ + iris::RenderCommand cmd{}; + const FakeLight new_value{}; + + cmd.set_light(&new_value); + + ASSERT_EQ(cmd.light(), &new_value); +} + +TEST(render_command_tests, equality) +{ + const auto type = iris::RenderCommandType::DRAW; + const iris::RenderPass render_pass{nullptr, nullptr, nullptr}; + const FakeMaterial material{}; + const iris::RenderEntity render_entity{nullptr, iris::Vector3{}}; + const FakeRenderTarget shadow_map{}; + const FakeLight light{}; + + const iris::RenderCommand cmd1{ + type, &render_pass, &material, &render_entity, &shadow_map, &light}; + + const iris::RenderCommand cmd2{ + type, &render_pass, &material, &render_entity, &shadow_map, &light}; + + ASSERT_EQ(cmd1, cmd2); +} + +TEST(render_command_tests, inequality) +{ + const auto type = iris::RenderCommandType::DRAW; + const iris::RenderPass render_pass{nullptr, nullptr, nullptr}; + const FakeMaterial material{}; + const iris::RenderEntity render_entity{nullptr, iris::Vector3{}}; + const FakeRenderTarget shadow_map{}; + const FakeLight light{}; + + const iris::RenderCommand cmd1{ + type, &render_pass, &material, &render_entity, &shadow_map, &light}; + + const iris::RenderCommand cmd2{ + type, &render_pass, nullptr, &render_entity, &shadow_map, &light}; + + ASSERT_NE(cmd1, cmd2); +} diff --git a/tests/graphics/render_queue_builder_tests.cpp b/tests/graphics/render_queue_builder_tests.cpp new file mode 100644 index 00000000..6fce69d0 --- /dev/null +++ b/tests/graphics/render_queue_builder_tests.cpp @@ -0,0 +1,181 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "graphics/material.h" +#include "graphics/render_pass.h" +#include "graphics/render_queue_builder.h" +#include "graphics/render_target.h" +#include "graphics/scene.h" + +#include "fakes/fake_material.h" +#include "fakes/fake_render_target.h" + +#include + +class RenderQueueBuilderFixture : public ::testing::Test +{ + public: + RenderQueueBuilderFixture() + : builder_(nullptr) + , materials_() + , render_targets_() + { + builder_ = std::make_unique( + [this](auto *, auto *, const auto *, auto) { + materials_.emplace_back(std::make_unique()); + return materials_.back().get(); + }, + [this](auto, auto) { + render_targets_.emplace_back( + std::make_unique()); + return render_targets_.back().get(); + }); + } + + protected: + std::unique_ptr builder_; + std::vector> materials_; + std::vector> render_targets_; + std::vector scenes_; + std::vector passes_; + + std::vector create_complex_scene() + { + render_targets_.emplace_back(std::make_unique()); + + scenes_.emplace_back(iris::Scene{}); + scenes_.emplace_back(iris::Scene{}); + auto &scene1 = scenes_[0]; + auto &scene2 = scenes_[1]; + + scene1.create_light(iris::Vector3{}, true); + scene1.create_entity(nullptr, nullptr, iris::Transform{}); + scene2.create_entity(nullptr, nullptr, iris::Transform{}); + + passes_.emplace_back( + std::addressof(scenes_[0]), nullptr, render_targets_.back().get()); + passes_.emplace_back(std::addressof(scenes_[1]), nullptr, nullptr); + + return passes_; + } +}; + +TEST_F(RenderQueueBuilderFixture, empty_passes) +{ + std::vector passes{}; + std::vector expected(1u); + expected[0u].set_type(iris::RenderCommandType::PRESENT); + + const auto queue = builder_->build(passes); + + ASSERT_EQ(queue, expected); +} + +TEST_F(RenderQueueBuilderFixture, complex_scene) +{ + auto passes = create_complex_scene(); + + const auto queue = builder_->build(passes); + + std::vector expected{ + {iris::RenderCommandType::PASS_START, + std::addressof(passes[0]), + nullptr, + nullptr, + nullptr, + nullptr}, + {iris::RenderCommandType::UPLOAD_TEXTURE, + std::addressof(passes[0]), + materials_[0].get(), + nullptr, + nullptr, + nullptr}, + {iris::RenderCommandType::DRAW, + std::addressof(passes[0]), + materials_[0].get(), + std::get<1>(scenes_[0].entities()[0]).get(), + nullptr, + scenes_[0].lighting_rig()->ambient_light.get()}, + {iris::RenderCommandType::PASS_END, + std::addressof(passes[0]), + materials_[0].get(), + std::get<1>(scenes_[0].entities()[0]).get(), + nullptr, + scenes_[0].lighting_rig()->ambient_light.get()}, + {iris::RenderCommandType::PASS_START, + std::addressof(passes[1]), + materials_[0].get(), + std::get<1>(scenes_[0].entities()[0]).get(), + nullptr, + scenes_[0].lighting_rig()->ambient_light.get()}, + {iris::RenderCommandType::UPLOAD_TEXTURE, + std::addressof(passes[1]), + materials_[1].get(), + std::get<1>(scenes_[0].entities()[0]).get(), + nullptr, + scenes_[0].lighting_rig()->ambient_light.get()}, + {iris::RenderCommandType::DRAW, + std::addressof(passes[1]), + materials_[1].get(), + std::get<1>(scenes_[0].entities()[0]).get(), + nullptr, + scenes_[0].lighting_rig()->ambient_light.get()}, + {iris::RenderCommandType::UPLOAD_TEXTURE, + std::addressof(passes[1]), + materials_[2].get(), + std::get<1>(scenes_[0].entities()[0]).get(), + nullptr, + scenes_[0].lighting_rig()->ambient_light.get()}, + {iris::RenderCommandType::DRAW, + std::addressof(passes[1]), + materials_[2].get(), + std::get<1>(scenes_[0].entities()[0]).get(), + nullptr, + scenes_[0].lighting_rig()->directional_lights[0].get()}, + {iris::RenderCommandType::PASS_END, + std::addressof(passes[1]), + materials_[2].get(), + std::get<1>(scenes_[0].entities()[0]).get(), + nullptr, + scenes_[0].lighting_rig()->directional_lights[0].get()}, + {iris::RenderCommandType::PASS_START, + std::addressof(passes[2]), + materials_[2].get(), + std::get<1>(scenes_[0].entities()[0]).get(), + nullptr, + scenes_[0].lighting_rig()->directional_lights[0].get()}, + {iris::RenderCommandType::UPLOAD_TEXTURE, + std::addressof(passes[2]), + materials_[3].get(), + std::get<1>(scenes_[0].entities()[0]).get(), + nullptr, + scenes_[0].lighting_rig()->directional_lights[0].get()}, + {iris::RenderCommandType::DRAW, + std::addressof(passes[2]), + materials_[3].get(), + std::get<1>(scenes_[1].entities()[0]).get(), + nullptr, + scenes_[1].lighting_rig()->ambient_light.get()}, + {iris::RenderCommandType::PASS_END, + std::addressof(passes[2]), + materials_[3].get(), + std::get<1>(scenes_[1].entities()[0]).get(), + nullptr, + scenes_[1].lighting_rig()->ambient_light.get()}, + {iris::RenderCommandType::PRESENT, + std::addressof(passes[2]), + materials_[3].get(), + std::get<1>(scenes_[1].entities()[0]).get(), + nullptr, + scenes_[1].lighting_rig()->ambient_light.get()}}; + + ASSERT_EQ(materials_.size(), 4u); + ASSERT_EQ(render_targets_.size(), 2u); + ASSERT_EQ(queue, expected); +} diff --git a/tests/graphics/renderer_tests.cpp b/tests/graphics/renderer_tests.cpp new file mode 100644 index 00000000..e0f4b248 --- /dev/null +++ b/tests/graphics/renderer_tests.cpp @@ -0,0 +1,37 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#include "fakes/fake_renderer.h" +#include "graphics/render_command.h" +#include "graphics/render_command_type.h" + +TEST(renderer_test, command_execution) +{ + const std::vector expected{ + iris::RenderCommandType::UPLOAD_TEXTURE, + iris::RenderCommandType::PASS_START, + iris::RenderCommandType::DRAW, + iris::RenderCommandType::PASS_END, + iris::RenderCommandType::PRESENT}; + + std::vector render_queue{}; + + for (const auto &command_type : expected) + { + iris::RenderCommand cmd; + cmd.set_type(command_type); + render_queue.emplace_back(cmd); + } + + FakeRenderer renderer{render_queue}; + + renderer.render(); + + ASSERT_EQ(renderer.call_log(), expected); +} diff --git a/tests/jobs/CMakeLists.txt b/tests/jobs/CMakeLists.txt new file mode 100644 index 00000000..aa1a5ebc --- /dev/null +++ b/tests/jobs/CMakeLists.txt @@ -0,0 +1,5 @@ +target_sources(unit_tests PRIVATE + concurrent_queue_tests.cpp + counter_tests.cpp + fiber_job_system_tests.cpp + thread_job_system_tests.cpp) \ No newline at end of file diff --git a/tests/jobs/concurrent_queue_tests.cpp b/tests/jobs/concurrent_queue_tests.cpp new file mode 100644 index 00000000..9873663c --- /dev/null +++ b/tests/jobs/concurrent_queue_tests.cpp @@ -0,0 +1,77 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include + +#include + +#include "jobs/concurrent_queue.h" + +TEST(concurrent_queue, constructor) +{ + iris::ConcurrentQueue q; + ASSERT_TRUE(q.empty()); +} + +TEST(concurrent_queue, enqueue) +{ + iris::ConcurrentQueue q; + q.enqueue(1); + + ASSERT_FALSE(q.empty()); +} + +TEST(concurrent_queue, try_dequeue) +{ + iris::ConcurrentQueue q; + q.enqueue(1); + int value = 0; + + ASSERT_TRUE(q.try_dequeue(value)); + ASSERT_TRUE(q.empty()); + ASSERT_EQ(value, 1); +} + +TEST(concurrent_queue, enqueue_thread_safe) +{ + static constexpr auto value_count = 10000; + iris::ConcurrentQueue q; + std::vector values(value_count); + std::iota(std::begin(values), std::end(values), 0); + + const auto worker = [&q, &values](int start) + { + for (int i = start; i < start + (value_count / 4); i++) + { + q.enqueue(values[i]); + } + }; + + std::thread thrd1{worker, (value_count / 4) * 0}; + std::thread thrd2{worker, (value_count / 4) * 1}; + std::thread thrd3{worker, (value_count / 4) * 2}; + std::thread thrd4{worker, (value_count / 4) * 3}; + + thrd1.join(); + thrd2.join(); + thrd3.join(); + thrd4.join(); + + std::vector popped; + + for (int i = 0; i < value_count; ++i) + { + int element = 0; + ASSERT_TRUE(q.try_dequeue(element)); + popped.emplace_back(element); + } + + std::sort(std::begin(popped), std::end(popped)); + ASSERT_EQ(popped, values); +} diff --git a/tests/jobs/counter_tests.cpp b/tests/jobs/counter_tests.cpp new file mode 100644 index 00000000..eaed4cbb --- /dev/null +++ b/tests/jobs/counter_tests.cpp @@ -0,0 +1,61 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include + +#include +#include + +#include "core/exception.h" +#include "jobs/fiber/counter.h" + +TEST(counter, constructor) +{ + iris::Counter ctr(3); + + ASSERT_EQ(static_cast(ctr), 3); +} + +TEST(counter, prefix_decrement) +{ + iris::Counter ctr(3); + --ctr; + + ASSERT_EQ(static_cast(ctr), 2); +} + +TEST(counter, postfix_decrement) +{ + iris::Counter ctr(3); + ctr--; + + ASSERT_EQ(static_cast(ctr), 2); +} + +TEST(counter, thread_safe) +{ + static constexpr auto value = 10000; + iris::Counter ctr(value); + + auto dec_thread = [&ctr]() { + for (auto i = 0; i < value / 4; ++i) + { + --ctr; + } + }; + + std::thread thrd1{dec_thread}; + std::thread thrd2{dec_thread}; + std::thread thrd3{dec_thread}; + std::thread thrd4{dec_thread}; + + thrd1.join(); + thrd2.join(); + thrd3.join(); + thrd4.join(); + + ASSERT_EQ(static_cast(ctr), 0); +} diff --git a/tests/jobs/fiber_job_system_tests.cpp b/tests/jobs/fiber_job_system_tests.cpp new file mode 100644 index 00000000..b766c803 --- /dev/null +++ b/tests/jobs/fiber_job_system_tests.cpp @@ -0,0 +1,11 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "jobs/job_system_tests.h" + +#include "jobs/fiber/fiber_job_system.h" + +INSTANTIATE_TYPED_TEST_SUITE_P(fiber, JobSystemTests, iris::FiberJobSystem); diff --git a/tests/jobs/job_system_tests.h b/tests/jobs/job_system_tests.h new file mode 100644 index 00000000..81484088 --- /dev/null +++ b/tests/jobs/job_system_tests.h @@ -0,0 +1,145 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include + +#include + +#include + +template +class JobSystemTests : public ::testing::Test +{ + protected: + T js_; +}; + +TYPED_TEST_SUITE_P(JobSystemTests); + +TYPED_TEST_P(JobSystemTests, add_jobs_single) +{ + std::atomic counter = 0; + + this->js_.add_jobs({[&counter]() { ++counter; }}); + + while (counter != 1) + { + } + + ASSERT_EQ(counter, 1); +} + +TYPED_TEST_P(JobSystemTests, add_jobs_multiple) +{ + std::atomic counter = 0; + + this->js_.add_jobs( + {[&counter]() { ++counter; }, + [&counter]() { ++counter; }, + [&counter]() { ++counter; }, + [&counter]() { ++counter; }}); + + while (counter != 4) + { + } + + ASSERT_EQ(counter, 4); +} + +TYPED_TEST_P(JobSystemTests, wait_for_jobs_single) +{ + std::atomic done = false; + + this->js_.wait_for_jobs({[&done]() { done = true; }}); + + ASSERT_TRUE(done); +} + +TYPED_TEST_P(JobSystemTests, wait_for_jobs_multiple) +{ + std::atomic counter = 0; + + this->js_.wait_for_jobs( + {[&counter]() { + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + ++counter; + }, + [&counter]() { ++counter; }, + [&counter]() { ++counter; }, + [&counter]() { ++counter; }}); + + ASSERT_EQ(counter, 4); +} + +TYPED_TEST_P(JobSystemTests, wait_for_jobs_nested) +{ + std::atomic counter = 0; + + this->js_.wait_for_jobs({[&counter, this]() { + this->js_.wait_for_jobs({[&counter, this]() { + this->js_.wait_for_jobs({[&counter]() { ++counter; }}); + ++counter; + }}); + ++counter; + }}); + + ASSERT_EQ(counter, 3); +} + +TYPED_TEST_P(JobSystemTests, wait_for_jobs_sequential) +{ + std::atomic counter = 0; + + this->js_.wait_for_jobs({[&counter]() { ++counter; }}); + this->js_.wait_for_jobs({[&counter]() { ++counter; }}); + + ASSERT_EQ(counter, 2); +} + +TYPED_TEST_P(JobSystemTests, exceptions_propagate) +{ + const auto throws = []() { throw std::runtime_error(""); }; + ASSERT_THROW(this->js_.wait_for_jobs({throws}), std::runtime_error); +} + +TYPED_TEST_P(JobSystemTests, exceptions_propagate_complex) +{ + ASSERT_THROW( + this->js_.wait_for_jobs( + {[this]() { + this->js_.wait_for_jobs( + {[]() {}, + [this]() { + this->js_.wait_for_jobs( + {[]() { throw std::runtime_error(""); }}); + }, + []() {}, + []() {}}); + }, + []() {}}), + std::runtime_error); +} + +TYPED_TEST_P(JobSystemTests, exceptions_propagate_first_job) +{ + ASSERT_THROW( + this->js_.wait_for_jobs({[]() { throw std::runtime_error(""); }}), + std::runtime_error); +} + +REGISTER_TYPED_TEST_SUITE_P( + JobSystemTests, + add_jobs_single, + add_jobs_multiple, + wait_for_jobs_single, + wait_for_jobs_multiple, + wait_for_jobs_nested, + wait_for_jobs_sequential, + exceptions_propagate, + exceptions_propagate_complex, + exceptions_propagate_first_job); diff --git a/tests/jobs/thread_job_system_tests.cpp b/tests/jobs/thread_job_system_tests.cpp new file mode 100644 index 00000000..0ce95a2e --- /dev/null +++ b/tests/jobs/thread_job_system_tests.cpp @@ -0,0 +1,11 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "jobs/job_system_tests.h" + +#include "jobs/thread/thread_job_system.h" + +INSTANTIATE_TYPED_TEST_SUITE_P(thread, JobSystemTests, iris::ThreadJobSystem); diff --git a/tests/networking/CMakeLists.txt b/tests/networking/CMakeLists.txt new file mode 100644 index 00000000..853a758f --- /dev/null +++ b/tests/networking/CMakeLists.txt @@ -0,0 +1,6 @@ +target_sources(unit_tests PRIVATE + data_buffer_serialiser_tests.cpp + packet_tests.cpp + reliable_ordered_channel_tests.cpp + unreliable_sequenced_channel_tests.cpp + unreliable_unordered_channel_tests.cpp) diff --git a/tests/networking/data_buffer_serialiser_tests.cpp b/tests/networking/data_buffer_serialiser_tests.cpp new file mode 100644 index 00000000..fc34ac89 --- /dev/null +++ b/tests/networking/data_buffer_serialiser_tests.cpp @@ -0,0 +1,156 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include + +#include "networking/data_buffer_deserialiser.h" +#include "networking/data_buffer_serialiser.h" + +TEST(data_buffer_seraliser_tests, constructor) +{ + iris::DataBufferSerialiser ser{}; + + ASSERT_TRUE(ser.data().empty()); +} + +TEST(data_buffer_seraliser_tests, 8bit_int) +{ + std::uint8_t val = 12u; + + iris::DataBufferSerialiser ser{}; + + ser.push(val); + iris::DataBufferDeserialiser der{ser.data()}; + + ASSERT_EQ(der.pop(), val); +} + +TEST(data_buffer_seraliser_tests, 16bit_int) +{ + std::uint16_t val = 12345u; + + iris::DataBufferSerialiser ser{}; + + ser.push(val); + iris::DataBufferDeserialiser der{ser.data()}; + + ASSERT_EQ(der.pop(), val); +} + +TEST(data_buffer_seraliser_tests, 32bit_int) +{ + std::uint32_t val = 123456789u; + + iris::DataBufferSerialiser ser{}; + + ser.push(val); + iris::DataBufferDeserialiser der{ser.data()}; + + ASSERT_EQ(der.pop(), val); +} + +TEST(data_buffer_seraliser_tests, 64bit_int) +{ + std::uint64_t val = 12345678901233456u; + + iris::DataBufferSerialiser ser{}; + + ser.push(val); + iris::DataBufferDeserialiser der{ser.data()}; + + ASSERT_EQ(der.pop(), val); +} + +TEST(data_buffer_seraliser_tests, enum) +{ + enum class Val : std::uint8_t + { + A = 12 + }; + + auto val = Val::A; + + iris::DataBufferSerialiser ser{}; + + ser.push(val); + iris::DataBufferDeserialiser der{ser.data()}; + + ASSERT_EQ(der.pop(), val); +} + +TEST(data_buffer_seraliser_tests, float) +{ + float val = 1.2345f; + + iris::DataBufferSerialiser ser{}; + + ser.push(val); + iris::DataBufferDeserialiser der{ser.data()}; + + ASSERT_EQ(der.pop(), val); +} + +TEST(data_buffer_seraliser_tests, vector3) +{ + iris::Vector3 val{1.1f, 2.2f, 3.3f}; + + iris::DataBufferSerialiser ser{}; + + ser.push(val); + iris::DataBufferDeserialiser der{ser.data()}; + + ASSERT_EQ(der.pop(), val); +} + +TEST(data_buffer_seraliser_tests, quaternion) +{ + iris::Quaternion val{1.1f, 2.2f, 3.3f, 4.4f}; + + iris::DataBufferSerialiser ser{}; + + ser.push(val); + iris::DataBufferDeserialiser der{ser.data()}; + + ASSERT_EQ(der.pop(), val); +} + +TEST(data_buffer_seraliser_tests, data_buffer) +{ + iris::DataBuffer val{ + static_cast(0x0), + static_cast(0x1), + static_cast(0x2)}; + + iris::DataBufferSerialiser ser{}; + + ser.push(val); + iris::DataBufferDeserialiser der{ser.data()}; + + ASSERT_EQ(der.pop(), val); +} + +TEST(data_buffer_seraliser_tests, complex) +{ + iris::DataBuffer val1{ + static_cast(0x0), + static_cast(0x1), + static_cast(0x2)}; + iris::Vector3 val2{1.1f, 2.2f, 3.3}; + std::int32_t val3 = -4; + + iris::DataBufferSerialiser ser{}; + + ser.push(val1); + ser.push(val2); + ser.push(val3); + iris::DataBufferDeserialiser der{ser.data()}; + + const auto &[pop1, pop2, pop3] = + der.pop_tuple(); + ASSERT_EQ(pop1, val1); + ASSERT_EQ(pop2, val2); + ASSERT_EQ(pop3, val3); +} diff --git a/tests/networking/helper.h b/tests/networking/helper.h new file mode 100644 index 00000000..01ca8621 --- /dev/null +++ b/tests/networking/helper.h @@ -0,0 +1,50 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include + +#include "core/data_buffer.h" +#include "networking/packet.h" + +static const iris::DataBuffer test_data{ + static_cast(0xaa), + static_cast(0xbb), + static_cast(0xcc), +}; + +/** + * Helper method to create a collection of packets with supplied sequence + * numbers and types. + * + * @param packet_sequence + * Collection of sequence numbers and types. + * + * @returns + * Packets created using supplied data. + */ +inline std::vector +create_packets(const std::vector> + &packet_sequence) +{ + std::vector packets; + + for (const auto &[sequence, type] : packet_sequence) + { + // acks don't have data + const auto data = + type == iris::PacketType::ACK ? iris::DataBuffer{} : test_data; + + iris::Packet packet{type, iris::ChannelType::RELIABLE_ORDERED, data}; + packet.set_sequence(sequence); + packets.emplace_back(packet); + } + + return packets; +} diff --git a/tests/networking/packet_tests.cpp b/tests/networking/packet_tests.cpp new file mode 100644 index 00000000..7efdf4a6 --- /dev/null +++ b/tests/networking/packet_tests.cpp @@ -0,0 +1,82 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include + +#include "core/exception.h" +#include "networking/packet.h" +#include "networking/packet_type.h" + +static const iris::DataBuffer test_data{ + static_cast(0x0), + static_cast(0x1), + static_cast(0x2)}; + +TEST(packet, construct_invalid) +{ + iris::Packet p{}; + + ASSERT_FALSE(p.is_valid()); +} + +TEST(packet, construct_normal) +{ + iris::Packet p{ + iris::PacketType::DATA, iris::ChannelType::RELIABLE_ORDERED, test_data}; + + ASSERT_TRUE(p.is_valid()); + ASSERT_EQ(p.type(), iris::PacketType::DATA); + ASSERT_EQ(p.channel(), iris::ChannelType::RELIABLE_ORDERED); + ASSERT_EQ(p.body_buffer(), test_data); + ASSERT_EQ(p.body_size(), test_data.size()); + ASSERT_EQ(p.sequence(), 0u); + ASSERT_EQ(iris::DataBuffer(p.body(), p.body() + p.body_size()), test_data); + ASSERT_EQ(std::memcmp(p.data(), &p, p.packet_size()), 0u); +} + +TEST(packet, construct_raw_packet) +{ + iris::Packet p1{ + iris::PacketType::DATA, iris::ChannelType::RELIABLE_ORDERED, test_data}; + + iris::DataBuffer raw_packet{p1.data(), p1.data() + p1.packet_size()}; + + iris::Packet p2{raw_packet}; + + ASSERT_EQ(p1, p2); +} + +TEST(packet, sequence) +{ + iris::Packet p{ + iris::PacketType::DATA, iris::ChannelType::RELIABLE_ORDERED, test_data}; + + p.set_sequence(10u); + + ASSERT_EQ(p.sequence(), 10u); +} + +TEST(packet, equality) +{ + iris::Packet p1{ + iris::PacketType::DATA, iris::ChannelType::RELIABLE_ORDERED, test_data}; + iris::Packet p2{ + iris::PacketType::DATA, iris::ChannelType::RELIABLE_ORDERED, test_data}; + + ASSERT_EQ(p1, p2); +} + +TEST(packet, inequality) +{ + iris::Packet p1{ + iris::PacketType::HELLO, + iris::ChannelType::RELIABLE_ORDERED, + test_data}; + iris::Packet p2{ + iris::PacketType::DATA, iris::ChannelType::RELIABLE_ORDERED, test_data}; + + ASSERT_NE(p1, p2); +} diff --git a/tests/networking/reliable_ordered_channel_tests.cpp b/tests/networking/reliable_ordered_channel_tests.cpp new file mode 100644 index 00000000..99b2e511 --- /dev/null +++ b/tests/networking/reliable_ordered_channel_tests.cpp @@ -0,0 +1,281 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include + +#include + +#include "networking/channel/reliable_ordered_channel.h" +#include "networking/packet.h" + +#include "helper.h" + +TEST(reliable_ordered_channel, unacked_packet_is_resent) +{ + const auto in_packets = create_packets({ + {0u, iris::PacketType::DATA}, + }); + iris::ReliableOrderedChannel channel{}; + + for (const auto &packet : in_packets) + { + channel.enqueue_send(packet); + } + + ASSERT_EQ(channel.yield_send_queue(), in_packets); + ASSERT_EQ(channel.yield_send_queue(), in_packets); + ASSERT_TRUE(channel.yield_receive_queue().empty()); +} + +TEST(reliable_ordered_channel, packet_sequence_set) +{ + const auto in_packets = create_packets({ + {0u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + }); + const auto expected = create_packets({ + {0u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + }); + iris::ReliableOrderedChannel channel{}; + + for (const auto &packet : in_packets) + { + channel.enqueue_send(packet); + } + + ASSERT_EQ(channel.yield_send_queue(), expected); + ASSERT_TRUE(channel.yield_receive_queue().empty()); +} + +TEST(reliable_ordered_channel, single_ack_single_packet) +{ + const auto in_packets = create_packets({ + {0u, iris::PacketType::DATA}, + }); + const auto out_packets = create_packets({ + {0u, iris::PacketType::ACK}, + }); + iris::ReliableOrderedChannel channel{}; + + for (const auto &packet : in_packets) + { + channel.enqueue_send(packet); + } + + for (const auto &packet : out_packets) + { + channel.enqueue_receive(packet); + } + + ASSERT_TRUE(channel.yield_send_queue().empty()); + ASSERT_TRUE(channel.yield_receive_queue().empty()); +} + +TEST(reliable_ordered_channel, single_ack_multi_packet) +{ + const auto in_packets = create_packets({ + {0u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + }); + const auto out_packets = create_packets({ + {1u, iris::PacketType::ACK}, + }); + const auto expected = create_packets({ + {0u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + }); + iris::ReliableOrderedChannel channel{}; + + for (const auto &packet : in_packets) + { + channel.enqueue_send(packet); + } + + for (const auto &packet : out_packets) + { + channel.enqueue_receive(packet); + } + + ASSERT_EQ(channel.yield_send_queue(), expected); + ASSERT_TRUE(channel.yield_receive_queue().empty()); +} + +TEST(reliable_ordered_channel, multi_ack_single_packet) +{ + const auto in_packets = create_packets({ + {0u, iris::PacketType::DATA}, + }); + const auto out_packets = create_packets({ + {1u, iris::PacketType::ACK}, + {2u, iris::PacketType::ACK}, + {3u, iris::PacketType::ACK}, + }); + iris::ReliableOrderedChannel channel{}; + + for (const auto &packet : in_packets) + { + channel.enqueue_send(packet); + } + + for (const auto &packet : out_packets) + { + channel.enqueue_receive(packet); + } + + ASSERT_EQ(channel.yield_send_queue(), in_packets); + ASSERT_TRUE(channel.yield_receive_queue().empty()); +} + +TEST(reliable_ordered_channel, multi_ack_multi_packet) +{ + const auto in_packets = create_packets({ + {0u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + }); + const auto out_packets = create_packets({ + {4u, iris::PacketType::ACK}, + {4u, iris::PacketType::ACK}, + {4u, iris::PacketType::ACK}, + {1u, iris::PacketType::ACK}, + {2u, iris::PacketType::ACK}, + {1u, iris::PacketType::ACK}, + {3u, iris::PacketType::ACK}, + {3u, iris::PacketType::ACK}, + {1u, iris::PacketType::ACK}, + {0u, iris::PacketType::ACK}, + }); + iris::ReliableOrderedChannel channel{}; + + for (const auto &packet : in_packets) + { + channel.enqueue_send(packet); + } + + for (const auto &packet : out_packets) + { + channel.enqueue_receive(packet); + } + + ASSERT_TRUE(channel.yield_send_queue().empty()); + ASSERT_TRUE(channel.yield_receive_queue().empty()); +} + +TEST(reliable_ordered_channel, single_out_acked) +{ + const auto in_packets = create_packets({ + {0u, iris::PacketType::ACK}, + }); + const auto out_packets = create_packets({ + {0u, iris::PacketType::DATA}, + }); + iris::ReliableOrderedChannel channel{}; + + for (const auto &packet : out_packets) + { + channel.enqueue_receive(packet); + } + + ASSERT_EQ(channel.yield_receive_queue(), out_packets); + ASSERT_EQ(channel.yield_send_queue(), in_packets); +} + +TEST(reliable_ordered_channel, multi_out_acked) +{ + const auto in_packets = create_packets({ + {0u, iris::PacketType::ACK}, + {1u, iris::PacketType::ACK}, + {2u, iris::PacketType::ACK}, + }); + const auto out_packets = create_packets({ + {0u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + }); + iris::ReliableOrderedChannel channel{}; + + for (const auto &packet : out_packets) + { + channel.enqueue_receive(packet); + } + + ASSERT_EQ(channel.yield_receive_queue(), out_packets); + ASSERT_EQ(channel.yield_send_queue(), in_packets); +} + +TEST(reliable_ordered_channel, multi_out_acked_unordered) +{ + const auto in_packets = create_packets({ + {2u, iris::PacketType::ACK}, + {0u, iris::PacketType::ACK}, + {1u, iris::PacketType::ACK}, + {3u, iris::PacketType::ACK}, + }); + const auto out_packets = create_packets({ + {2u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {3u, iris::PacketType::DATA}, + }); + const auto expected = create_packets({ + {0u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + {3u, iris::PacketType::DATA}, + }); + iris::ReliableOrderedChannel channel{}; + + for (const auto &packet : out_packets) + { + channel.enqueue_receive(packet); + } + + ASSERT_EQ(channel.yield_receive_queue(), expected); + ASSERT_EQ(channel.yield_send_queue(), in_packets); +} + +TEST(reliable_ordered_channel, multi_out_acked_early_yield) +{ + const auto in_packets = create_packets({ + {3u, iris::PacketType::ACK}, + {1u, iris::PacketType::ACK}, + {0u, iris::PacketType::ACK}, + {2u, iris::PacketType::ACK}, + }); + const auto out_packets = create_packets({ + {3u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + }); + const auto expected1 = create_packets({ + {0u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + }); + const auto expected2 = create_packets({ + {2u, iris::PacketType::DATA}, + {3u, iris::PacketType::DATA}, + }); + iris::ReliableOrderedChannel channel{}; + + channel.enqueue_receive(out_packets[0u]); + channel.enqueue_receive(out_packets[1u]); + channel.enqueue_receive(out_packets[2u]); + const auto out_queue1 = channel.yield_receive_queue(); + channel.enqueue_receive(out_packets[3u]); + const auto out_queue2 = channel.yield_receive_queue(); + const auto out_queue3 = channel.yield_receive_queue(); + + ASSERT_EQ(out_queue1, expected1); + ASSERT_EQ(out_queue2, expected2); + ASSERT_TRUE(out_queue3.empty()); + ASSERT_EQ(channel.yield_send_queue(), in_packets); +} diff --git a/tests/networking/unreliable_sequenced_channel_tests.cpp b/tests/networking/unreliable_sequenced_channel_tests.cpp new file mode 100644 index 00000000..4908e5bb --- /dev/null +++ b/tests/networking/unreliable_sequenced_channel_tests.cpp @@ -0,0 +1,292 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include + +#include + +#include "networking/channel/unreliable_sequenced_channel.h" +#include "networking/packet.h" + +#include "helper.h" + +TEST(unreliable_sequenced_channel, in_queue_single) +{ + const auto in_packets = create_packets({ + {0u, iris::PacketType::DATA}, + }); + iris::UnreliableSequencedChannel channel{}; + + for (const auto &packet : in_packets) + { + channel.enqueue_send(packet); + } + + ASSERT_EQ(channel.yield_send_queue(), in_packets); + ASSERT_TRUE(channel.yield_send_queue().empty()); +} + +TEST(unreliable_sequenced_channel, in_queue_multi) +{ + const auto in_packets = create_packets({ + {0u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + }); + iris::UnreliableSequencedChannel channel{}; + + for (const auto &packet : in_packets) + { + channel.enqueue_send(packet); + } + + ASSERT_EQ(channel.yield_send_queue(), in_packets); + ASSERT_TRUE(channel.yield_send_queue().empty()); +} + +TEST(unreliable_sequenced_channel, in_queue_multi_incrementing_seq) +{ + const auto in_packets = create_packets({ + {0u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + }); + const auto expected = create_packets({ + {0u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + }); + iris::UnreliableSequencedChannel channel{}; + + for (const auto &packet : in_packets) + { + channel.enqueue_send(packet); + } + + ASSERT_EQ(channel.yield_send_queue(), expected); + ASSERT_TRUE(channel.yield_send_queue().empty()); +} + +TEST(unreliable_sequenced_channel, in_queue_multi_early_yield) +{ + const auto in_packets = create_packets({ + {0u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + }); + iris::UnreliableSequencedChannel channel{}; + std::vector> yielded_packets{}; + + channel.enqueue_send(in_packets[0u]); + channel.enqueue_send(in_packets[1u]); + yielded_packets.emplace_back(channel.yield_send_queue()); + yielded_packets.emplace_back(channel.yield_send_queue()); + channel.enqueue_send(in_packets[2u]); + yielded_packets.emplace_back(channel.yield_send_queue()); + yielded_packets.emplace_back(channel.yield_send_queue()); + + ASSERT_EQ(yielded_packets.size(), 4u); + ASSERT_EQ(yielded_packets[0u].size(), 2u); + ASSERT_EQ( + yielded_packets[0u], + std::vector( + std::cbegin(in_packets), std::cbegin(in_packets) + 2u)); + ASSERT_TRUE(yielded_packets[1u].empty()); + ASSERT_EQ(yielded_packets[2u].size(), 1u); + ASSERT_EQ( + yielded_packets[2u], + std::vector( + std::cbegin(in_packets) + 2u, std::cend(in_packets))); + ASSERT_TRUE(yielded_packets[3u].empty()); +} + +TEST(unreliable_sequenced_channel, out_queue_single) +{ + const auto out_packets = create_packets({ + {0u, iris::PacketType::DATA}, + }); + iris::UnreliableSequencedChannel channel{}; + + for (const auto &packet : out_packets) + { + channel.enqueue_receive(packet); + } + + ASSERT_EQ(channel.yield_receive_queue(), out_packets); + ASSERT_TRUE(channel.yield_receive_queue().empty()); +} + +TEST(unreliable_sequenced_channel, out_queue_multi) +{ + const auto out_packets = create_packets({ + {0u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + }); + iris::UnreliableSequencedChannel channel{}; + + for (const auto &packet : out_packets) + { + channel.enqueue_receive(packet); + } + + ASSERT_EQ(channel.yield_receive_queue(), out_packets); + ASSERT_TRUE(channel.yield_receive_queue().empty()); +} + +TEST(unreliable_sequenced_channel, out_queue_duplicates) +{ + const auto out_packets = create_packets({ + {0u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + }); + const auto expected = create_packets({ + {0u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + }); + iris::UnreliableSequencedChannel channel{}; + + for (const auto &packet : out_packets) + { + channel.enqueue_receive(packet); + } + + ASSERT_EQ(channel.yield_receive_queue(), expected); + ASSERT_TRUE(channel.yield_receive_queue().empty()); +} + +TEST(unreliable_sequenced_channel, out_queue_unordered) +{ + const auto out_packets = create_packets({ + {1u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {4u, iris::PacketType::DATA}, + {3u, iris::PacketType::DATA}, + {5u, iris::PacketType::DATA}, + {7u, iris::PacketType::DATA}, + }); + const auto expected = create_packets({ + {1u, iris::PacketType::DATA}, + {4u, iris::PacketType::DATA}, + {5u, iris::PacketType::DATA}, + {7u, iris::PacketType::DATA}, + }); + iris::UnreliableSequencedChannel channel{}; + + for (const auto &packet : out_packets) + { + channel.enqueue_receive(packet); + } + + ASSERT_EQ(channel.yield_receive_queue(), expected); + ASSERT_TRUE(channel.yield_receive_queue().empty()); +} + +TEST(unreliable_sequenced_channel, out_queue_unordered_and_duplicates) +{ + const auto out_packets = create_packets({ + {1u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {4u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {3u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {5u, iris::PacketType::DATA}, + {4u, iris::PacketType::DATA}, + {5u, iris::PacketType::DATA}, + {5u, iris::PacketType::DATA}, + {7u, iris::PacketType::DATA}, + {5u, iris::PacketType::DATA}, + }); + const auto expected = create_packets({ + {1u, iris::PacketType::DATA}, + {4u, iris::PacketType::DATA}, + {5u, iris::PacketType::DATA}, + {7u, iris::PacketType::DATA}, + }); + iris::UnreliableSequencedChannel channel{}; + + for (const auto &packet : out_packets) + { + channel.enqueue_receive(packet); + } + + ASSERT_EQ(channel.yield_receive_queue(), expected); + ASSERT_TRUE(channel.yield_receive_queue().empty()); +} + +TEST( + unreliable_sequenced_channel, + out_queue_unordered_and_duplicates_early_yield) +{ + const auto out_packets = create_packets({ + {1u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {4u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {3u, iris::PacketType::DATA}, + {0u, iris::PacketType::DATA}, + {5u, iris::PacketType::DATA}, + {4u, iris::PacketType::DATA}, + {5u, iris::PacketType::DATA}, + {5u, iris::PacketType::DATA}, + {7u, iris::PacketType::DATA}, + {5u, iris::PacketType::DATA}, + }); + const auto expected1 = create_packets({ + {1u, iris::PacketType::DATA}, + {4u, iris::PacketType::DATA}, + }); + const auto expected2 = create_packets({ + {5u, iris::PacketType::DATA}, + {7u, iris::PacketType::DATA}, + }); + iris::UnreliableSequencedChannel channel{}; + std::vector> yielded_packets{}; + + channel.enqueue_receive(out_packets[0u]); + channel.enqueue_receive(out_packets[1u]); + channel.enqueue_receive(out_packets[2u]); + channel.enqueue_receive(out_packets[3u]); + channel.enqueue_receive(out_packets[4u]); + yielded_packets.emplace_back(channel.yield_receive_queue()); + yielded_packets.emplace_back(channel.yield_receive_queue()); + channel.enqueue_receive(out_packets[5u]); + channel.enqueue_receive(out_packets[6u]); + channel.enqueue_receive(out_packets[7u]); + channel.enqueue_receive(out_packets[8u]); + channel.enqueue_receive(out_packets[9u]); + channel.enqueue_receive(out_packets[10u]); + channel.enqueue_receive(out_packets[11u]); + channel.enqueue_receive(out_packets[12u]); + channel.enqueue_receive(out_packets[12u]); + channel.enqueue_receive(out_packets[13u]); + yielded_packets.emplace_back(channel.yield_receive_queue()); + yielded_packets.emplace_back(channel.yield_receive_queue()); + channel.enqueue_receive(out_packets[14u]); + yielded_packets.emplace_back(channel.yield_receive_queue()); + + ASSERT_EQ(yielded_packets.size(), 5u); + ASSERT_EQ(yielded_packets[0u], expected1); + ASSERT_TRUE(yielded_packets[1u].empty()); + ASSERT_EQ(yielded_packets[2u], expected2); + ASSERT_TRUE(yielded_packets[3u].empty()); + ASSERT_TRUE(yielded_packets[4u].empty()); +} diff --git a/tests/networking/unreliable_unordered_channel_tests.cpp b/tests/networking/unreliable_unordered_channel_tests.cpp new file mode 100644 index 00000000..27da4903 --- /dev/null +++ b/tests/networking/unreliable_unordered_channel_tests.cpp @@ -0,0 +1,152 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include + +#include "core/data_buffer.h" +#include "networking/channel/unreliable_unordered_channel.h" +#include "networking/packet.h" + +#include "helper.h" + +TEST(unreliable_unordered_channel, in_queue_single) +{ + const auto in_packets = create_packets({ + {0u, iris::PacketType::DATA}, + }); + iris::UnreliableUnorderedChannel channel{}; + + for (const auto &packet : in_packets) + { + channel.enqueue_send(packet); + } + + ASSERT_EQ(channel.yield_send_queue(), in_packets); + ASSERT_TRUE(channel.yield_send_queue().empty()); +} + +TEST(unreliable_unordered_channel, in_queue_multi) +{ + const auto in_packets = create_packets({ + {0u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + }); + iris::UnreliableUnorderedChannel channel{}; + + for (const auto &packet : in_packets) + { + channel.enqueue_send(packet); + } + + ASSERT_EQ(channel.yield_send_queue(), in_packets); + ASSERT_TRUE(channel.yield_send_queue().empty()); +} + +TEST(unreliable_unordered_channel, in_queue_multi_early_yield) +{ + const auto in_packets = create_packets({ + {0u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + }); + iris::UnreliableUnorderedChannel channel{}; + std::vector> yielded_packets{}; + + channel.enqueue_send(in_packets[0u]); + channel.enqueue_send(in_packets[1u]); + yielded_packets.emplace_back(channel.yield_send_queue()); + yielded_packets.emplace_back(channel.yield_send_queue()); + channel.enqueue_send(in_packets[2u]); + yielded_packets.emplace_back(channel.yield_send_queue()); + yielded_packets.emplace_back(channel.yield_send_queue()); + + ASSERT_EQ(yielded_packets.size(), 4u); + ASSERT_EQ(yielded_packets[0u].size(), 2u); + ASSERT_EQ( + yielded_packets[0u], + std::vector( + std::cbegin(in_packets), std::cbegin(in_packets) + 2u)); + ASSERT_TRUE(yielded_packets[1u].empty()); + ASSERT_EQ(yielded_packets[2u].size(), 1u); + ASSERT_EQ( + yielded_packets[2u], + std::vector( + std::cbegin(in_packets) + 2u, std::cend(in_packets))); + ASSERT_TRUE(yielded_packets[3u].empty()); +} + +TEST(unreliable_unordered_channel, out_queue_single) +{ + const auto out_packets = create_packets({ + {0u, iris::PacketType::DATA}, + }); + iris::UnreliableUnorderedChannel channel{}; + + for (const auto &packet : out_packets) + { + channel.enqueue_receive(packet); + } + + ASSERT_EQ(channel.yield_receive_queue(), out_packets); + ASSERT_TRUE(channel.yield_receive_queue().empty()); +} + +TEST(unreliable_unordered_channel, out_queue_multi) +{ + const auto out_packets = create_packets({ + {0u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + }); + iris::UnreliableUnorderedChannel channel{}; + + for (const auto &packet : out_packets) + { + channel.enqueue_receive(packet); + } + + ASSERT_EQ(channel.yield_receive_queue(), out_packets); + ASSERT_TRUE(channel.yield_receive_queue().empty()); +} + +TEST(unreliable_unordered_channel, out_queue_multi_early_yield) +{ + const auto out_packets = create_packets({ + {0u, iris::PacketType::DATA}, + {1u, iris::PacketType::DATA}, + {2u, iris::PacketType::DATA}, + }); + iris::UnreliableUnorderedChannel channel{}; + std::vector> yielded_packets{}; + + channel.enqueue_receive(out_packets[0u]); + channel.enqueue_receive(out_packets[1u]); + yielded_packets.emplace_back(channel.yield_receive_queue()); + yielded_packets.emplace_back(channel.yield_receive_queue()); + channel.enqueue_receive(out_packets[2u]); + yielded_packets.emplace_back(channel.yield_receive_queue()); + yielded_packets.emplace_back(channel.yield_receive_queue()); + + ASSERT_EQ(yielded_packets.size(), 4u); + ASSERT_EQ(yielded_packets[0u].size(), 2u); + ASSERT_EQ( + yielded_packets[0u], + std::vector( + std::cbegin(out_packets), std::cbegin(out_packets) + 2u)); + ASSERT_TRUE(yielded_packets[1u].empty()); + ASSERT_EQ(yielded_packets[2u].size(), 1u); + ASSERT_EQ( + yielded_packets[2u], + std::vector( + std::cbegin(out_packets) + 2u, std::cend(out_packets))); + ASSERT_TRUE(yielded_packets[3u].empty()); +} diff --git a/tests/platform/CMakeLists.txt b/tests/platform/CMakeLists.txt new file mode 100644 index 00000000..a4fe5fdb --- /dev/null +++ b/tests/platform/CMakeLists.txt @@ -0,0 +1,3 @@ +if(IRIS_PLATFORM MATCHES "MACOS") + add_subdirectory("macos") +endif() diff --git a/tests/platform/macos/CMakeLists.txt b/tests/platform/macos/CMakeLists.txt new file mode 100644 index 00000000..8d63a1f4 --- /dev/null +++ b/tests/platform/macos/CMakeLists.txt @@ -0,0 +1,2 @@ +target_sources(unit_tests PRIVATE + thread_tests.cpp) diff --git a/tests/platform/macos/thread_tests.cpp b/tests/platform/macos/thread_tests.cpp new file mode 100644 index 00000000..bace62f7 --- /dev/null +++ b/tests/platform/macos/thread_tests.cpp @@ -0,0 +1,42 @@ +//////////////////////////////////////////////////////////////////////////////// +// Distributed under the Boost Software License, Version 1.0. // +// (See accompanying file LICENSE or copy at // +// https://www.boost.org/LICENSE_1_0.txt) // +//////////////////////////////////////////////////////////////////////////////// + +#include "core/thread.h" + +#include +#include + +#include + +#include "core/exception.h" + +TEST(thread, default_constructor) +{ + iris::Thread thrd{}; + ASSERT_FALSE(thrd.joinable()); +} + +TEST(thread, function_constructor) +{ + std::atomic done = false; + + iris::Thread thrd{[](std::atomic *done) { *done = true; }, &done}; + + ASSERT_TRUE(thrd.joinable()); + + thrd.join(); + + ASSERT_TRUE(done); + ASSERT_FALSE(thrd.joinable()); +} + +TEST(thread, invalid_bind) +{ + iris::Thread thrd{}; + ASSERT_THROW( + thrd.bind_to_core(std::thread::hardware_concurrency()), + iris::Exception); +} diff --git a/toolchains/ios.toolchain.cmake b/toolchains/ios.toolchain.cmake new file mode 100644 index 00000000..5a9359df --- /dev/null +++ b/toolchains/ios.toolchain.cmake @@ -0,0 +1,732 @@ +# This file is part of the ios-cmake project. It was retrieved from +# https://github.com/cristeab/ios-cmake.git, which is a fork of +# https://code.google.com/p/ios-cmake/. Which in turn is based off of +# the Platform/Darwin.cmake and Platform/UnixPaths.cmake files which +# are included with CMake 2.8.4 +# +# The ios-cmake project is licensed under the new BSD license. +# +# Copyright (c) 2014, Bogdan Cristea and LTE Engineering Software, +# Kitware, Inc., Insight Software Consortium. All rights reserved. +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# This file is based off of the Platform/Darwin.cmake and +# Platform/UnixPaths.cmake files which are included with CMake 2.8.4 +# It has been altered for iOS development. +# +# Updated by Alex Stewart (alexs.mac@gmail.com) +# +# ***************************************************************************** +# Now maintained by Alexander Widerberg (widerbergaren [at] gmail.com) +# under the BSD-3-Clause license +# https://github.com/leetal/ios-cmake +# ***************************************************************************** +# +# INFORMATION / HELP +# +# The following arguments control the behaviour of this toolchain: +# +# PLATFORM: (default "OS") +# OS = Build for iPhoneOS. +# OS64 = Build for arm64 iphoneOS. +# OS64COMBINED = Build for arm64 x86_64 iphoneOS. Combined into FAT STATIC lib (supported on 3.14+ of CMakewith "-G Xcode" argument ONLY) +# SIMULATOR = Build for x86 i386 iphoneOS Simulator. +# SIMULATOR64 = Build for x86_64 iphoneOS Simulator. +# TVOS = Build for arm64 tvOS. +# TVOSCOMBINED = Build for arm64 x86_64 tvOS. Combined into FAT STATIC lib (supported on 3.14+ of CMake with "-G Xcode" argument ONLY) +# SIMULATOR_TVOS = Build for x86_64 tvOS Simulator. +# WATCHOS = Build for armv7k arm64_32 for watchOS. +# WATCHOSCOMBINED = Build for armv7k arm64_32 x86_64 watchOS. Combined into FAT STATIC lib (supported on 3.14+ of CMake with "-G Xcode" argument ONLY) +# SIMULATOR_WATCHOS = Build for x86_64 for watchOS Simulator. +# +# CMAKE_OSX_SYSROOT: Path to the SDK to use. By default this is +# automatically determined from PLATFORM and xcodebuild, but +# can also be manually specified (although this should not be required). +# +# CMAKE_DEVELOPER_ROOT: Path to the Developer directory for the platform +# being compiled for. By default this is automatically determined from +# CMAKE_OSX_SYSROOT, but can also be manually specified (although this should +# not be required). +# +# DEPLOYMENT_TARGET: Minimum SDK version to target. Default 2.0 on watchOS and 9.0 on tvOS+iOS +# +# ENABLE_BITCODE: (1|0) Enables or disables bitcode support. Default 1 (true) +# +# ENABLE_ARC: (1|0) Enables or disables ARC support. Default 1 (true, ARC enabled by default) +# +# ENABLE_VISIBILITY: (1|0) Enables or disables symbol visibility support. Default 0 (false, visibility hidden by default) +# +# ENABLE_STRICT_TRY_COMPILE: (1|0) Enables or disables strict try_compile() on all Check* directives (will run linker +# to actually check if linking is possible). Default 0 (false, will set CMAKE_TRY_COMPILE_TARGET_TYPE to STATIC_LIBRARY) +# +# ARCHS: (armv7 armv7s armv7k arm64 arm64_32 i386 x86_64) If specified, will override the default architectures for the given PLATFORM +# OS = armv7 armv7s arm64 (if applicable) +# OS64 = arm64 (if applicable) +# SIMULATOR = i386 +# SIMULATOR64 = x86_64 +# TVOS = arm64 +# SIMULATOR_TVOS = x86_64 (i386 has since long been deprecated) +# WATCHOS = armv7k arm64_32 (if applicable) +# SIMULATOR_WATCHOS = x86_64 (i386 has since long been deprecated) +# +# This toolchain defines the following variables for use externally: +# +# XCODE_VERSION: Version number (not including Build version) of Xcode detected. +# SDK_VERSION: Version of SDK being used. +# CMAKE_OSX_ARCHITECTURES: Architectures being compiled for (generated from PLATFORM). +# APPLE_TARGET_TRIPLE: Used by autoconf build systems. NOTE: If "ARCHS" are overridden, this will *NOT* be set! +# +# This toolchain defines the following macros for use externally: +# +# set_xcode_property (TARGET XCODE_PROPERTY XCODE_VALUE XCODE_VARIANT) +# A convenience macro for setting xcode specific properties on targets. +# Available variants are: All, Release, RelWithDebInfo, Debug, MinSizeRel +# example: set_xcode_property (myioslib IPHONEOS_DEPLOYMENT_TARGET "3.1" "all"). +# +# find_host_package (PROGRAM ARGS) +# A macro used to find executable programs on the host system, not within the +# environment. Thanks to the android-cmake project for providing the +# command. +# +# ******************************** DEPRECATIONS ******************************* +# +# IOS_DEPLOYMENT_TARGET: (Deprecated) Alias to DEPLOYMENT_TARGET +# CMAKE_IOS_DEVELOPER_ROOT: (Deprecated) Alias to CMAKE_DEVELOPER_ROOT +# IOS_PLATFORM: (Deprecated) Alias to PLATFORM +# IOS_ARCH: (Deprecated) Alias to ARCHS +# +# ***************************************************************************** +# + +# iris specific setup +set(IRIS_PLATFORM IOS) +set(IRIS_ARCH ARM64) +set(ENABLE_ARC 1) +set(ENABLE_VISABILITY 1) +set(PLATFORM OS64) + +# Fix for PThread library not in path +set(CMAKE_THREAD_LIBS_INIT "-lpthread") +set(CMAKE_HAVE_THREADS_LIBRARY 1) +set(CMAKE_USE_WIN32_THREADS_INIT 0) +set(CMAKE_USE_PTHREADS_INIT 1) + +# Cache what generator is used +set(USED_CMAKE_GENERATOR "${CMAKE_GENERATOR}" CACHE STRING "Expose CMAKE_GENERATOR" FORCE) + +if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.14") + set(MODERN_CMAKE YES) +endif() + +# Get the Xcode version being used. +execute_process(COMMAND xcodebuild -version + OUTPUT_VARIABLE XCODE_VERSION + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) +string(REGEX MATCH "Xcode [0-9\\.]+" XCODE_VERSION "${XCODE_VERSION}") +string(REGEX REPLACE "Xcode ([0-9\\.]+)" "\\1" XCODE_VERSION "${XCODE_VERSION}") + +######## ALIASES (DEPRECATION WARNINGS) + +if(DEFINED IOS_PLATFORM) + set(PLATFORM ${IOS_PLATFORM}) + message(DEPRECATION "IOS_PLATFORM argument is DEPRECATED. Consider using the new PLATFORM argument instead.") +endif() + +if(DEFINED IOS_DEPLOYMENT_TARGET) + set(DEPLOYMENT_TARGET ${IOS_DEPLOYMENT_TARGET}) + message(DEPRECATION "IOS_DEPLOYMENT_TARGET argument is DEPRECATED. Consider using the new DEPLOYMENT_TARGET argument instead.") +endif() + +if(DEFINED CMAKE_IOS_DEVELOPER_ROOT) + set(CMAKE_DEVELOPER_ROOT ${CMAKE_IOS_DEVELOPER_ROOT}) + message(DEPRECATION "CMAKE_IOS_DEVELOPER_ROOT argument is DEPRECATED. Consider using the new CMAKE_DEVELOPER_ROOT argument instead.") +endif() + +if(DEFINED IOS_ARCH) + set(ARCHS ${IOS_ARCH}) + message(DEPRECATION "IOS_ARCH argument is DEPRECATED. Consider using the new ARCHS argument instead.") +endif() + +######## END ALIASES + +# Unset the FORCE on cache variables if in try_compile() +set(FORCE_CACHE FORCE) +get_property(_CMAKE_IN_TRY_COMPILE GLOBAL PROPERTY IN_TRY_COMPILE) +if(_CMAKE_IN_TRY_COMPILE) + unset(FORCE_CACHE) +endif() + +# Default to building for iPhoneOS if not specified otherwise, and we cannot +# determine the platform from the CMAKE_OSX_ARCHITECTURES variable. The use +# of CMAKE_OSX_ARCHITECTURES is such that try_compile() projects can correctly +# determine the value of PLATFORM from the root project, as +# CMAKE_OSX_ARCHITECTURES is propagated to them by CMake. +if(NOT DEFINED PLATFORM) + if (CMAKE_OSX_ARCHITECTURES) + if(CMAKE_OSX_ARCHITECTURES MATCHES ".*arm.*" AND CMAKE_OSX_SYSROOT MATCHES ".*iphoneos.*") + set(PLATFORM "OS") + elseif(CMAKE_OSX_ARCHITECTURES MATCHES "i386" AND CMAKE_OSX_SYSROOT MATCHES ".*iphonesimulator.*") + set(PLATFORM "SIMULATOR") + elseif(CMAKE_OSX_ARCHITECTURES MATCHES "x86_64" AND CMAKE_OSX_SYSROOT MATCHES ".*iphonesimulator.*") + set(PLATFORM "SIMULATOR64") + elseif(CMAKE_OSX_ARCHITECTURES MATCHES "arm64" AND CMAKE_OSX_SYSROOT MATCHES ".*appletvos.*") + set(PLATFORM "TVOS") + elseif(CMAKE_OSX_ARCHITECTURES MATCHES "x86_64" AND CMAKE_OSX_SYSROOT MATCHES ".*appletvsimulator.*") + set(PLATFORM "SIMULATOR_TVOS") + elseif(CMAKE_OSX_ARCHITECTURES MATCHES ".*armv7k.*" AND CMAKE_OSX_SYSROOT MATCHES ".*watchos.*") + set(PLATFORM "WATCHOS") + elseif(CMAKE_OSX_ARCHITECTURES MATCHES "i386" AND CMAKE_OSX_SYSROOT MATCHES ".*watchsimulator.*") + set(PLATFORM "SIMULATOR_WATCHOS") + endif() + endif() + if (NOT PLATFORM) + set(PLATFORM "OS") + endif() +endif() + +set(PLATFORM_INT "${PLATFORM}" CACHE STRING "Type of platform for which the build targets.") + +# Handle the case where we are targeting iOS and a version above 10.3.4 (32-bit support dropped officially) +if(PLATFORM_INT STREQUAL "OS" AND DEPLOYMENT_TARGET VERSION_GREATER_EQUAL 10.3.4) + set(PLATFORM_INT "OS64") + message(STATUS "Targeting minimum SDK version ${DEPLOYMENT_TARGET}. Dropping 32-bit support.") +elseif(PLATFORM_INT STREQUAL "SIMULATOR" AND DEPLOYMENT_TARGET VERSION_GREATER_EQUAL 10.3.4) + set(PLATFORM_INT "SIMULATOR64") + message(STATUS "Targeting minimum SDK version ${DEPLOYMENT_TARGET}. Dropping 32-bit support.") +endif() + +# Determine the platform name and architectures for use in xcodebuild commands +# from the specified PLATFORM name. +if(PLATFORM_INT STREQUAL "OS") + set(SDK_NAME iphoneos) + if(NOT ARCHS) + set(ARCHS armv7 armv7s arm64) + set(APPLE_TARGET_TRIPLE_INT arm-apple-ios) + endif() +elseif(PLATFORM_INT STREQUAL "OS64") + set(SDK_NAME iphoneos) + if(NOT ARCHS) + if (XCODE_VERSION VERSION_GREATER 10.0) + set(ARCHS arm64) # Add arm64e when Apple have fixed the integration issues with it, libarclite_iphoneos.a is currently missung bitcode markers for example + else() + set(ARCHS arm64) + endif() + set(APPLE_TARGET_TRIPLE_INT aarch64-apple-ios) + endif() +elseif(PLATFORM_INT STREQUAL "OS64COMBINED") + set(SDK_NAME iphoneos) + if(MODERN_CMAKE) + if(NOT ARCHS) + if (XCODE_VERSION VERSION_GREATER 10.0) + set(ARCHS arm64 x86_64) # Add arm64e when Apple have fixed the integration issues with it, libarclite_iphoneos.a is currently missung bitcode markers for example + else() + set(ARCHS arm64 x86_64) + endif() + set(APPLE_TARGET_TRIPLE_INT aarch64-x86_64-apple-ios) + endif() + else() + message(FATAL_ERROR "Please make sure that you are running CMake 3.14+ to make the OS64COMBINED setting work") + endif() +elseif(PLATFORM_INT STREQUAL "SIMULATOR") + set(SDK_NAME iphonesimulator) + if(NOT ARCHS) + set(ARCHS i386) + set(APPLE_TARGET_TRIPLE_INT i386-apple-ios) + endif() + message(DEPRECATION "SIMULATOR IS DEPRECATED. Consider using SIMULATOR64 instead.") +elseif(PLATFORM_INT STREQUAL "SIMULATOR64") + set(SDK_NAME iphonesimulator) + if(NOT ARCHS) + set(ARCHS x86_64) + set(APPLE_TARGET_TRIPLE_INT x86_64-apple-ios) + endif() +elseif(PLATFORM_INT STREQUAL "TVOS") + set(SDK_NAME appletvos) + if(NOT ARCHS) + set(ARCHS arm64) + set(APPLE_TARGET_TRIPLE_INT aarch64-apple-tvos) + endif() +elseif (PLATFORM_INT STREQUAL "TVOSCOMBINED") + set(SDK_NAME appletvos) + if(MODERN_CMAKE) + if(NOT ARCHS) + set(ARCHS arm64 x86_64) + set(APPLE_TARGET_TRIPLE_INT aarch64-x86_64-apple-tvos) + endif() + else() + message(FATAL_ERROR "Please make sure that you are running CMake 3.14+ to make the TVOSCOMBINED setting work") + endif() +elseif(PLATFORM_INT STREQUAL "SIMULATOR_TVOS") + set(SDK_NAME appletvsimulator) + if(NOT ARCHS) + set(ARCHS x86_64) + set(APPLE_TARGET_TRIPLE_INT x86_64-apple-tvos) + endif() +elseif(PLATFORM_INT STREQUAL "WATCHOS") + set(SDK_NAME watchos) + if(NOT ARCHS) + if (XCODE_VERSION VERSION_GREATER 10.0) + set(ARCHS armv7k arm64_32) + set(APPLE_TARGET_TRIPLE_INT aarch64_32-apple-watchos) + else() + set(ARCHS armv7k) + set(APPLE_TARGET_TRIPLE_INT arm-apple-watchos) + endif() + endif() +elseif(PLATFORM_INT STREQUAL "WATCHOSCOMBINED") + set(SDK_NAME watchos) + if(MODERN_CMAKE) + if(NOT ARCHS) + if (XCODE_VERSION VERSION_GREATER 10.0) + set(ARCHS armv7k arm64_32 i386) + set(APPLE_TARGET_TRIPLE_INT aarch64_32-i386-apple-watchos) + else() + set(ARCHS armv7k i386) + set(APPLE_TARGET_TRIPLE_INT arm-i386-apple-watchos) + endif() + endif() + else() + message(FATAL_ERROR "Please make sure that you are running CMake 3.14+ to make the WATCHOSCOMBINED setting work") + endif() +elseif(PLATFORM_INT STREQUAL "SIMULATOR_WATCHOS") + set(SDK_NAME watchsimulator) + if(NOT ARCHS) + set(ARCHS i386) + set(APPLE_TARGET_TRIPLE_INT i386-apple-watchos) + endif() +else() + message(FATAL_ERROR "Invalid PLATFORM: ${PLATFORM_INT}") +endif() + +if(MODERN_CMAKE AND PLATFORM_INT MATCHES ".*COMBINED" AND NOT USED_CMAKE_GENERATOR MATCHES "Xcode") + message(FATAL_ERROR "The COMBINED options only work with Xcode generator, -G Xcode") +endif() + +# If user did not specify the SDK root to use, then query xcodebuild for it. +execute_process(COMMAND xcodebuild -version -sdk ${SDK_NAME} Path + OUTPUT_VARIABLE CMAKE_OSX_SYSROOT_INT + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) +if (NOT DEFINED CMAKE_OSX_SYSROOT_INT AND NOT DEFINED CMAKE_OSX_SYSROOT) + message(SEND_ERROR "Please make sure that Xcode is installed and that the toolchain" + "is pointing to the correct path. Please run:" + "sudo xcode-select -s /Applications/Xcode.app/Contents/Developer" + "and see if that fixes the problem for you.") + message(FATAL_ERROR "Invalid CMAKE_OSX_SYSROOT: ${CMAKE_OSX_SYSROOT} " + "does not exist.") +elseif(DEFINED CMAKE_OSX_SYSROOT_INT) + set(CMAKE_OSX_SYSROOT "${CMAKE_OSX_SYSROOT_INT}" CACHE INTERNAL "") +endif() + +# Set Xcode property for SDKROOT as well if Xcode generator is used +if(USED_CMAKE_GENERATOR MATCHES "Xcode") + set(CMAKE_OSX_SYSROOT "${SDK_NAME}" CACHE INTERNAL "") + if(NOT DEFINED CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM) + set(CMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "123456789A" CACHE INTERNAL "") + endif() +endif() + +# Specify minimum version of deployment target. +if(NOT DEFINED DEPLOYMENT_TARGET) + if (PLATFORM_INT STREQUAL "WATCHOS" OR PLATFORM_INT STREQUAL "SIMULATOR_WATCHOS") + # Unless specified, SDK version 2.0 is used by default as minimum target version (watchOS). + set(DEPLOYMENT_TARGET "2.0" + CACHE STRING "Minimum SDK version to build for." ) + else() + # Unless specified, SDK version 9.0 is used by default as minimum target version (iOS, tvOS). + set(DEPLOYMENT_TARGET "9.0" + CACHE STRING "Minimum SDK version to build for." ) + endif() + message(STATUS "Using the default min-version since DEPLOYMENT_TARGET not provided!") +endif() + +# Use bitcode or not +if(NOT DEFINED ENABLE_BITCODE AND NOT ARCHS MATCHES "((^|;|, )(i386|x86_64))+") + # Unless specified, enable bitcode support by default + message(STATUS "Enabling bitcode support by default. ENABLE_BITCODE not provided!") + set(ENABLE_BITCODE TRUE) +elseif(NOT DEFINED ENABLE_BITCODE) + message(STATUS "Disabling bitcode support by default on simulators. ENABLE_BITCODE not provided for override!") + set(ENABLE_BITCODE FALSE) +endif() +set(ENABLE_BITCODE_INT ${ENABLE_BITCODE} CACHE BOOL "Whether or not to enable bitcode" ${FORCE_CACHE}) +# Use ARC or not +if(NOT DEFINED ENABLE_ARC) + # Unless specified, enable ARC support by default + set(ENABLE_ARC TRUE) + message(STATUS "Enabling ARC support by default. ENABLE_ARC not provided!") +endif() +set(ENABLE_ARC_INT ${ENABLE_ARC} CACHE BOOL "Whether or not to enable ARC" ${FORCE_CACHE}) +# Use hidden visibility or not +if(NOT DEFINED ENABLE_VISIBILITY) + # Unless specified, disable symbols visibility by default + set(ENABLE_VISIBILITY FALSE) + message(STATUS "Hiding symbols visibility by default. ENABLE_VISIBILITY not provided!") +endif() +set(ENABLE_VISIBILITY_INT ${ENABLE_VISIBILITY} CACHE BOOL "Whether or not to hide symbols (-fvisibility=hidden)" ${FORCE_CACHE}) +# Set strict compiler checks or not +if(NOT DEFINED ENABLE_STRICT_TRY_COMPILE) + # Unless specified, disable strict try_compile() + set(ENABLE_STRICT_TRY_COMPILE FALSE) + message(STATUS "Using NON-strict compiler checks by default. ENABLE_STRICT_TRY_COMPILE not provided!") +endif() +set(ENABLE_STRICT_TRY_COMPILE_INT ${ENABLE_STRICT_TRY_COMPILE} CACHE BOOL "Whether or not to use strict compiler checks" ${FORCE_CACHE}) +# Get the SDK version information. +execute_process(COMMAND xcodebuild -sdk ${CMAKE_OSX_SYSROOT} -version SDKVersion + OUTPUT_VARIABLE SDK_VERSION + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) + +# Find the Developer root for the specific iOS platform being compiled for +# from CMAKE_OSX_SYSROOT. Should be ../../ from SDK specified in +# CMAKE_OSX_SYSROOT. There does not appear to be a direct way to obtain +# this information from xcrun or xcodebuild. +if (NOT DEFINED CMAKE_DEVELOPER_ROOT AND NOT USED_CMAKE_GENERATOR MATCHES "Xcode") + get_filename_component(PLATFORM_SDK_DIR ${CMAKE_OSX_SYSROOT} PATH) + get_filename_component(CMAKE_DEVELOPER_ROOT ${PLATFORM_SDK_DIR} PATH) + if (NOT DEFINED CMAKE_DEVELOPER_ROOT) + message(FATAL_ERROR "Invalid CMAKE_DEVELOPER_ROOT: " + "${CMAKE_DEVELOPER_ROOT} does not exist.") + endif() +endif() +# Find the C & C++ compilers for the specified SDK. +if(NOT CMAKE_C_COMPILER) + execute_process(COMMAND xcrun -sdk ${CMAKE_OSX_SYSROOT} -find clang + OUTPUT_VARIABLE CMAKE_C_COMPILER + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) + message(STATUS "Using C compiler: ${CMAKE_C_COMPILER}") +endif() +if(NOT CMAKE_CXX_COMPILER) + execute_process(COMMAND xcrun -sdk ${CMAKE_OSX_SYSROOT} -find clang++ + OUTPUT_VARIABLE CMAKE_CXX_COMPILER + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) + message(STATUS "Using CXX compiler: ${CMAKE_CXX_COMPILER}") +endif() +# Find (Apple's) libtool. +execute_process(COMMAND xcrun -sdk ${CMAKE_OSX_SYSROOT} -find libtool + OUTPUT_VARIABLE BUILD_LIBTOOL + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) +message(STATUS "Using libtool: ${BUILD_LIBTOOL}") +# Configure libtool to be used instead of ar + ranlib to build static libraries. +# This is required on Xcode 7+, but should also work on previous versions of +# Xcode. +set(CMAKE_C_CREATE_STATIC_LIBRARY + "${BUILD_LIBTOOL} -static -o ") +set(CMAKE_CXX_CREATE_STATIC_LIBRARY + "${BUILD_LIBTOOL} -static -o ") +# Find the toolchain's provided install_name_tool if none is found on the host +if(NOT CMAKE_INSTALL_NAME_TOOL) + execute_process(COMMAND xcrun -sdk ${CMAKE_OSX_SYSROOT} -find install_name_tool + OUTPUT_VARIABLE CMAKE_INSTALL_NAME_TOOL_INT + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) + set(CMAKE_INSTALL_NAME_TOOL ${CMAKE_INSTALL_NAME_TOOL_INT} CACHE STRING "" ${FORCE_CACHE}) +endif() +# Get the version of Darwin (OS X) of the host. +execute_process(COMMAND uname -r + OUTPUT_VARIABLE CMAKE_HOST_SYSTEM_VERSION + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) +if(SDK_NAME MATCHES "iphone") + set(CMAKE_SYSTEM_NAME iOS CACHE INTERNAL "" ${FORCE_CACHE}) +endif() +# CMake 3.14+ support building for iOS, watchOS and tvOS out of the box. +if(MODERN_CMAKE) + if(SDK_NAME MATCHES "appletv") + set(CMAKE_SYSTEM_NAME tvOS CACHE INTERNAL "" ${FORCE_CACHE}) + elseif(SDK_NAME MATCHES "watch") + set(CMAKE_SYSTEM_NAME watchOS CACHE INTERNAL "" ${FORCE_CACHE}) + endif() + # Provide flags for a combined FAT library build on newer CMake versions + if(PLATFORM_INT MATCHES ".*COMBINED") + set(CMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH "NO" CACHE INTERNAL "" ${FORCE_CACHE}) + set(CMAKE_IOS_INSTALL_COMBINED YES CACHE INTERNAL "" ${FORCE_CACHE}) + message(STATUS "Will combine built (static) artifacts into FAT lib...") + endif() +elseif(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.10") + # Legacy code path prior to CMake 3.14 or fallback if no SDK_NAME specified + set(CMAKE_SYSTEM_NAME iOS CACHE INTERNAL "" ${FORCE_CACHE}) +else() + # Legacy code path prior to CMake 3.14 or fallback if no SDK_NAME specified + set(CMAKE_SYSTEM_NAME Darwin CACHE INTERNAL "" ${FORCE_CACHE}) +endif() +# Standard settings. +set(CMAKE_SYSTEM_VERSION ${SDK_VERSION} CACHE INTERNAL "") +set(UNIX TRUE CACHE BOOL "") +set(APPLE TRUE CACHE BOOL "") +set(IOS TRUE CACHE BOOL "") +set(CMAKE_AR ar CACHE FILEPATH "" FORCE) +set(CMAKE_RANLIB ranlib CACHE FILEPATH "" FORCE) +set(CMAKE_STRIP strip CACHE FILEPATH "" FORCE) +# Set the architectures for which to build. +set(CMAKE_OSX_ARCHITECTURES ${ARCHS} CACHE STRING "Build architecture for iOS") +# Change the type of target generated for try_compile() so it'll work when cross-compiling, weak compiler checks +if(ENABLE_STRICT_TRY_COMPILE_INT) + message(STATUS "Using strict compiler checks (default in CMake).") +else() + set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) +endif() +# All iOS/Darwin specific settings - some may be redundant. +set(CMAKE_MACOSX_BUNDLE YES) +set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO") +set(CMAKE_SHARED_LIBRARY_PREFIX "lib") +set(CMAKE_SHARED_LIBRARY_SUFFIX ".dylib") +set(CMAKE_SHARED_MODULE_PREFIX "lib") +set(CMAKE_SHARED_MODULE_SUFFIX ".so") +set(CMAKE_C_COMPILER_ABI ELF) +set(CMAKE_CXX_COMPILER_ABI ELF) +set(CMAKE_C_HAS_ISYSROOT 1) +set(CMAKE_CXX_HAS_ISYSROOT 1) +set(CMAKE_MODULE_EXISTS 1) +set(CMAKE_DL_LIBS "") +set(CMAKE_C_OSX_COMPATIBILITY_VERSION_FLAG "-compatibility_version ") +set(CMAKE_C_OSX_CURRENT_VERSION_FLAG "-current_version ") +set(CMAKE_CXX_OSX_COMPATIBILITY_VERSION_FLAG "${CMAKE_C_OSX_COMPATIBILITY_VERSION_FLAG}") +set(CMAKE_CXX_OSX_CURRENT_VERSION_FLAG "${CMAKE_C_OSX_CURRENT_VERSION_FLAG}") + +if(ARCHS MATCHES "((^|;|, )(arm64|arm64e|x86_64))+") + set(CMAKE_C_SIZEOF_DATA_PTR 8) + set(CMAKE_CXX_SIZEOF_DATA_PTR 8) + if(ARCHS MATCHES "((^|;|, )(arm64|arm64e))+") + set(CMAKE_SYSTEM_PROCESSOR "aarch64") + else() + set(CMAKE_SYSTEM_PROCESSOR "x86_64") + endif() +else() + set(CMAKE_C_SIZEOF_DATA_PTR 4) + set(CMAKE_CXX_SIZEOF_DATA_PTR 4) + set(CMAKE_SYSTEM_PROCESSOR "arm") +endif() + +# Note that only Xcode 7+ supports the newer more specific: +# -m${SDK_NAME}-version-min flags, older versions of Xcode use: +# -m(ios/ios-simulator)-version-min instead. +if(${CMAKE_VERSION} VERSION_LESS "3.11") + if(PLATFORM_INT STREQUAL "OS" OR PLATFORM_INT STREQUAL "OS64") + if(XCODE_VERSION VERSION_LESS 7.0) + set(SDK_NAME_VERSION_FLAGS + "-mios-version-min=${DEPLOYMENT_TARGET}") + else() + # Xcode 7.0+ uses flags we can build directly from SDK_NAME. + set(SDK_NAME_VERSION_FLAGS + "-m${SDK_NAME}-version-min=${DEPLOYMENT_TARGET}") + endif() + elseif(PLATFORM_INT STREQUAL "TVOS") + set(SDK_NAME_VERSION_FLAGS + "-mtvos-version-min=${DEPLOYMENT_TARGET}") + elseif(PLATFORM_INT STREQUAL "SIMULATOR_TVOS") + set(SDK_NAME_VERSION_FLAGS + "-mtvos-simulator-version-min=${DEPLOYMENT_TARGET}") + elseif(PLATFORM_INT STREQUAL "WATCHOS") + set(SDK_NAME_VERSION_FLAGS + "-mwatchos-version-min=${DEPLOYMENT_TARGET}") + elseif(PLATFORM_INT STREQUAL "SIMULATOR_WATCHOS") + set(SDK_NAME_VERSION_FLAGS + "-mwatchos-simulator-version-min=${DEPLOYMENT_TARGET}") + else() + # SIMULATOR or SIMULATOR64 both use -mios-simulator-version-min. + set(SDK_NAME_VERSION_FLAGS + "-mios-simulator-version-min=${DEPLOYMENT_TARGET}") + endif() +else() + # Newer versions of CMake sets the version min flags correctly + set(CMAKE_OSX_DEPLOYMENT_TARGET ${DEPLOYMENT_TARGET} CACHE STRING + "Set CMake deployment target" ${FORCE_CACHE}) +endif() + +if(DEFINED APPLE_TARGET_TRIPLE_INT) + set(APPLE_TARGET_TRIPLE ${APPLE_TARGET_TRIPLE_INT} CACHE STRING + "Autoconf target triple compatible variable" ${FORCE_CACHE}) +endif() + +if(ENABLE_BITCODE_INT) + set(BITCODE "-fembed-bitcode") + set(CMAKE_XCODE_ATTRIBUTE_BITCODE_GENERATION_MODE "bitcode" CACHE INTERNAL "") + set(CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE "YES" CACHE INTERNAL "") +else() + set(BITCODE "") + set(CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE "NO" CACHE INTERNAL "") +endif() + +if(ENABLE_ARC_INT) + set(FOBJC_ARC "-fobjc-arc") + set(CMAKE_XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC "YES" CACHE INTERNAL "") +else() + set(FOBJC_ARC "-fno-objc-arc") + set(CMAKE_XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC "NO" CACHE INTERNAL "") +endif() + +if(NOT ENABLE_VISIBILITY_INT) + set(VISIBILITY "-fvisibility=hidden") + set(CMAKE_XCODE_ATTRIBUTE_GCC_SYMBOLS_PRIVATE_EXTERN "YES" CACHE INTERNAL "") +else() + set(VISIBILITY "") + set(CMAKE_XCODE_ATTRIBUTE_GCC_SYMBOLS_PRIVATE_EXTERN "NO" CACHE INTERNAL "") +endif() + +if(NOT IOS_TOOLCHAIN_HAS_RUN) + #Check if Xcode generator is used, since that will handle these flags automagically + if(USED_CMAKE_GENERATOR MATCHES "Xcode") + message(STATUS "Not setting any manual command-line buildflags, since Xcode is selected as generator.") + else() + set(CMAKE_C_FLAGS + "${SDK_NAME_VERSION_FLAGS} ${BITCODE} -fobjc-abi-version=2 ${FOBJC_ARC} ${CMAKE_C_FLAGS}") + # Hidden visibilty is required for C++ on iOS. + set(CMAKE_CXX_FLAGS + "${SDK_NAME_VERSION_FLAGS} ${BITCODE} ${VISIBILITY} -fvisibility-inlines-hidden -fobjc-abi-version=2 ${FOBJC_ARC} ${CMAKE_CXX_FLAGS}") + set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS} -O0 -g ${CMAKE_CXX_FLAGS_DEBUG}") + set(CMAKE_CXX_FLAGS_MINSIZEREL "${CMAKE_CXX_FLAGS} -DNDEBUG -Os -ffast-math ${CMAKE_CXX_FLAGS_MINSIZEREL}") + set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS} -DNDEBUG -O2 -g -ffast-math ${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") + set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS} -DNDEBUG -O3 -ffast-math ${CMAKE_CXX_FLAGS_RELEASE}") + set(CMAKE_C_LINK_FLAGS "${SDK_NAME_VERSION_FLAGS} -Wl,-search_paths_first ${CMAKE_C_LINK_FLAGS}") + set(CMAKE_CXX_LINK_FLAGS "${SDK_NAME_VERSION_FLAGS} -Wl,-search_paths_first ${CMAKE_CXX_LINK_FLAGS}") + set(CMAKE_ASM_FLAGS "${CFLAGS} -x assembler-with-cpp") + + # In order to ensure that the updated compiler flags are used in try_compile() + # tests, we have to forcibly set them in the CMake cache, not merely set them + # in the local scope. + set(VARS_TO_FORCE_IN_CACHE + CMAKE_C_FLAGS + CMAKE_CXX_FLAGS + CMAKE_CXX_FLAGS_DEBUG + CMAKE_CXX_FLAGS_RELWITHDEBINFO + CMAKE_CXX_FLAGS_MINSIZEREL + CMAKE_CXX_FLAGS_RELEASE + CMAKE_C_LINK_FLAGS + CMAKE_CXX_LINK_FLAGS) + foreach(VAR_TO_FORCE ${VARS_TO_FORCE_IN_CACHE}) + set(${VAR_TO_FORCE} "${${VAR_TO_FORCE}}" CACHE STRING "" ${FORCE_CACHE}) + endforeach() + endif() + + ## Print status messages to inform of the current state + message(STATUS "Configuring ${SDK_NAME} build for platform: ${PLATFORM_INT}, architecture(s): ${ARCHS}") + message(STATUS "Using SDK: ${CMAKE_OSX_SYSROOT_INT}") + if(DEFINED APPLE_TARGET_TRIPLE) + message(STATUS "Autoconf target triple: ${APPLE_TARGET_TRIPLE}") + endif() + message(STATUS "Using minimum deployment version: ${DEPLOYMENT_TARGET}" + " (SDK version: ${SDK_VERSION})") + if(MODERN_CMAKE) + message(STATUS "Merging integrated CMake 3.14+ iOS,tvOS,watchOS,macOS toolchain(s) with this toolchain!") + endif() + if(USED_CMAKE_GENERATOR MATCHES "Xcode") + message(STATUS "Using Xcode version: ${XCODE_VERSION}") + endif() + if(DEFINED SDK_NAME_VERSION_FLAGS) + message(STATUS "Using version flags: ${SDK_NAME_VERSION_FLAGS}") + endif() + message(STATUS "Using a data_ptr size of: ${CMAKE_CXX_SIZEOF_DATA_PTR}") + message(STATUS "Using install_name_tool: ${CMAKE_INSTALL_NAME_TOOL}") + if(ENABLE_BITCODE_INT) + message(STATUS "Enabling bitcode support.") + else() + message(STATUS "Disabling bitcode support.") + endif() + + if(ENABLE_ARC_INT) + message(STATUS "Enabling ARC support.") + else() + message(STATUS "Disabling ARC support.") + endif() + + if(NOT ENABLE_VISIBILITY_INT) + message(STATUS "Hiding symbols (-fvisibility=hidden).") + endif() +endif() + +set(CMAKE_PLATFORM_HAS_INSTALLNAME 1) +set(CMAKE_SHARED_LINKER_FLAGS "-rpath @executable_path/Frameworks -rpath @loader_path/Frameworks") +set(CMAKE_SHARED_LIBRARY_CREATE_C_FLAGS "-dynamiclib -Wl,-headerpad_max_install_names") +set(CMAKE_SHARED_MODULE_CREATE_C_FLAGS "-bundle -Wl,-headerpad_max_install_names") +set(CMAKE_SHARED_MODULE_LOADER_C_FLAG "-Wl,-bundle_loader,") +set(CMAKE_SHARED_MODULE_LOADER_CXX_FLAG "-Wl,-bundle_loader,") +set(CMAKE_FIND_LIBRARY_SUFFIXES ".tbd" ".dylib" ".so" ".a") +set(CMAKE_SHARED_LIBRARY_SONAME_C_FLAG "-install_name") + +# Set the find root to the iOS developer roots and to user defined paths. +set(CMAKE_FIND_ROOT_PATH ${CMAKE_OSX_SYSROOT_INT} ${CMAKE_PREFIX_PATH} CACHE STRING "Root path that will be prepended + to all search paths") +# Default to searching for frameworks first. +set(CMAKE_FIND_FRAMEWORK FIRST) +# Set up the default search directories for frameworks. +set(CMAKE_FRAMEWORK_PATH + ${CMAKE_DEVELOPER_ROOT}/Library/PrivateFrameworks + ${CMAKE_OSX_SYSROOT_INT}/System/Library/Frameworks + ${CMAKE_FRAMEWORK_PATH} CACHE STRING "Frameworks search paths" ${FORCE_CACHE}) + +set(IOS_TOOLCHAIN_HAS_RUN TRUE CACHE BOOL "Has the CMake toolchain run already?" ${FORCE_CACHE}) + +# By default, search both the specified iOS SDK and the remainder of the host filesystem. +if(NOT CMAKE_FIND_ROOT_PATH_MODE_PROGRAM) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM BOTH CACHE STRING "" ${FORCE_CACHE}) +endif() +if(NOT CMAKE_FIND_ROOT_PATH_MODE_LIBRARY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH CACHE STRING "" ${FORCE_CACHE}) +endif() +if(NOT CMAKE_FIND_ROOT_PATH_MODE_INCLUDE) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH CACHE STRING "" ${FORCE_CACHE}) +endif() +if(NOT CMAKE_FIND_ROOT_PATH_MODE_PACKAGE) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH CACHE STRING "" ${FORCE_CACHE}) +endif() + +# +# Some helper-macros below to simplify and beautify the CMakeFile +# + +# This little macro lets you set any Xcode specific property. +macro(set_xcode_property TARGET XCODE_PROPERTY XCODE_VALUE XCODE_RELVERSION) + set(XCODE_RELVERSION_I "${XCODE_RELVERSION}") + if(XCODE_RELVERSION_I STREQUAL "All") + set_property(TARGET ${TARGET} PROPERTY + XCODE_ATTRIBUTE_${XCODE_PROPERTY} "${XCODE_VALUE}") + else() + set_property(TARGET ${TARGET} PROPERTY + XCODE_ATTRIBUTE_${XCODE_PROPERTY}[variant=${XCODE_RELVERSION_I}] "${XCODE_VALUE}") + endif() +endmacro(set_xcode_property) + +# This macro lets you find executable programs on the host system. +macro(find_host_package) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE NEVER) + set(IOS FALSE) + find_package(${ARGN}) + set(IOS TRUE) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM BOTH) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH) +endmacro(find_host_package)