├── .gitignore ├── tests ├── main.cpp └── framegraph_test.cpp ├── docs └── images │ ├── example.png │ └── example2.png ├── include └── fg │ ├── realize.hpp │ ├── render_task_builder.hpp │ ├── render_task.hpp │ ├── render_task_base.hpp │ ├── resource_base.hpp │ ├── resource.hpp │ └── framegraph.hpp ├── cmake ├── assign_source_group.cmake └── import_library.cmake ├── license.txt ├── conanfile.py ├── CMakeLists.txt └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | *build/* -------------------------------------------------------------------------------- /tests/main.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include "catch.hpp" 3 | -------------------------------------------------------------------------------- /docs/images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acdemiralp/fg/HEAD/docs/images/example.png -------------------------------------------------------------------------------- /docs/images/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acdemiralp/fg/HEAD/docs/images/example2.png -------------------------------------------------------------------------------- /include/fg/realize.hpp: -------------------------------------------------------------------------------- 1 | #ifndef FG_REALIZE_HPP_ 2 | #define FG_REALIZE_HPP_ 3 | 4 | #include 5 | #include 6 | 7 | namespace fg 8 | { 9 | template 10 | struct missing_realize_implementation : std::false_type {}; 11 | 12 | template 13 | std::unique_ptr realize(const description_type& description) 14 | { 15 | static_assert(missing_realize_implementation::value, "Missing realize implementation for description - type pair."); 16 | return nullptr; 17 | } 18 | } 19 | 20 | #endif -------------------------------------------------------------------------------- /cmake/assign_source_group.cmake: -------------------------------------------------------------------------------- 1 | # Assigns the given files to source groups identical to their location. 2 | function(assign_source_group) 3 | foreach(_SOURCE IN ITEMS ${ARGN}) 4 | if (IS_ABSOLUTE "${_SOURCE}") 5 | file(RELATIVE_PATH _SOURCE_REL "${CMAKE_CURRENT_SOURCE_DIR}" "${_SOURCE}") 6 | else() 7 | set(_SOURCE_REL "${_SOURCE}") 8 | endif() 9 | get_filename_component(_SOURCE_PATH "${_SOURCE_REL}" PATH) 10 | if(WIN32) 11 | string(REPLACE "/" "\\" _SOURCE_PATH_MSVC "${_SOURCE_PATH}") 12 | source_group("${_SOURCE_PATH_MSVC}" FILES "${_SOURCE}") 13 | else() 14 | source_group("${_SOURCE_PATH}" FILES "${_SOURCE}") 15 | endif() 16 | endforeach() 17 | endfunction(assign_source_group) 18 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ali Can Demiralp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmake/import_library.cmake: -------------------------------------------------------------------------------- 1 | # Imports a library which is not built with cmake. 2 | # The include directories are appended to the PROJECT_INCLUDE_DIRS variable. 3 | # The libraries are appended to the PROJECT_LIBRARIES variable. 4 | # Usage: 5 | # Header Only: 6 | # import_library(INCLUDE_DIRS) 7 | # Identical Debug and Release: 8 | # import_library(INCLUDE_DIRS LIBRARIES) 9 | # Separate Debug and Release: 10 | # import_library(INCLUDE_DIRS DEBUG_LIBRARIES RELEASE_LIBRARIES) 11 | function(import_library INCLUDE_DIRS) 12 | set (PROJECT_INCLUDE_DIRS ${PROJECT_INCLUDE_DIRS} ${${INCLUDE_DIRS}} PARENT_SCOPE) 13 | set (_EXTRA_ARGS ${ARGN}) 14 | list(LENGTH _EXTRA_ARGS _EXTRA_ARGS_LENGTH) 15 | if (_EXTRA_ARGS_LENGTH EQUAL 1) 16 | list(GET _EXTRA_ARGS 0 _LIBRARIES) 17 | set (PROJECT_LIBRARIES ${PROJECT_LIBRARIES} ${${_LIBRARIES}} PARENT_SCOPE) 18 | elseif(_EXTRA_ARGS_LENGTH EQUAL 2) 19 | list(GET _EXTRA_ARGS 0 _DEBUG_LIBRARIES ) 20 | list(GET _EXTRA_ARGS 1 _RELEASE_LIBRARIES) 21 | set (PROJECT_LIBRARIES ${PROJECT_LIBRARIES} debug ${${_DEBUG_LIBRARIES}} optimized ${${_RELEASE_LIBRARIES}} PARENT_SCOPE) 22 | endif () 23 | endfunction(import_library) 24 | -------------------------------------------------------------------------------- /conanfile.py: -------------------------------------------------------------------------------- 1 | from conans import ConanFile, CMake, tools 2 | from conans.tools import download, unzip 3 | import os 4 | 5 | class FgConan(ConanFile): 6 | name = "fg" 7 | version = "1.1.0" 8 | description = "Conan package for fg." 9 | url = "https://github.com/acdemiralp/fg" 10 | license = "MIT" 11 | settings = "arch", "build_type", "compiler", "os" 12 | generators = "cmake" 13 | 14 | def source(self): 15 | zip_name = "%s.zip" % self.version 16 | download ("%s/archive/%s" % (self.url, zip_name), zip_name, verify=False) 17 | unzip (zip_name) 18 | os.unlink(zip_name) 19 | 20 | def build(self): 21 | cmake = CMake(self) 22 | self.run("cmake %s-%s %s" % (self.name, self.version, cmake.command_line)) 23 | self.run("cmake --build . %s" % cmake.build_config) 24 | 25 | def package(self): 26 | include_folder = "%s-%s/include" % (self.name, self.version) 27 | self.copy("*.h" , dst="include", src=include_folder) 28 | self.copy("*.hpp", dst="include", src=include_folder) 29 | self.copy("*.inl", dst="include", src=include_folder) 30 | -------------------------------------------------------------------------------- /include/fg/render_task_builder.hpp: -------------------------------------------------------------------------------- 1 | #ifndef FG_RENDER_TASK_BUILDER_HPP_ 2 | #define FG_RENDER_TASK_BUILDER_HPP_ 3 | 4 | #include 5 | 6 | namespace fg 7 | { 8 | class framegraph; 9 | class render_task_base; 10 | 11 | // The interface between the framegraph and a render task. 12 | class render_task_builder 13 | { 14 | public: 15 | explicit render_task_builder (framegraph* framegraph, render_task_base* render_task) : framegraph_(framegraph), render_task_(render_task) 16 | { 17 | 18 | } 19 | render_task_builder (const render_task_builder& that) = default; 20 | render_task_builder ( render_task_builder&& temp) = default; 21 | virtual ~render_task_builder () = default; 22 | render_task_builder& operator=(const render_task_builder& that) = default; 23 | render_task_builder& operator=( render_task_builder&& temp) = default; 24 | 25 | template 26 | resource_type* create(const std::string& name, const description_type& description); 27 | template 28 | resource_type* read (resource_type* resource); 29 | template 30 | resource_type* write (resource_type* resource); 31 | 32 | protected: 33 | framegraph* framegraph_ ; 34 | render_task_base* render_task_; 35 | }; 36 | } 37 | 38 | #endif -------------------------------------------------------------------------------- /include/fg/render_task.hpp: -------------------------------------------------------------------------------- 1 | #ifndef FG_RENDER_TASK_HPP_ 2 | #define FG_RENDER_TASK_HPP_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | namespace fg 10 | { 11 | class render_task_builder; 12 | 13 | template 14 | class render_task : public render_task_base 15 | { 16 | public: 17 | using data_type = data_type_; 18 | 19 | explicit render_task ( 20 | const std::string& name , 21 | const std::function& setup , 22 | const std::function& execute) : render_task_base(name), setup_(setup), execute_(execute) 23 | { 24 | 25 | } 26 | render_task (const render_task& that) = delete ; 27 | render_task ( render_task&& temp) = default; 28 | virtual ~render_task () = default; 29 | render_task& operator=(const render_task& that) = delete ; 30 | render_task& operator=( render_task&& temp) = default; 31 | 32 | const data_type& data() const 33 | { 34 | return data_; 35 | } 36 | 37 | protected: 38 | void setup (render_task_builder& builder) override 39 | { 40 | setup_ (data_, builder); 41 | } 42 | void execute() const override 43 | { 44 | execute_(data_); 45 | } 46 | 47 | data_type data_ ; 48 | const std::function setup_ ; 49 | const std::function execute_; 50 | }; 51 | } 52 | 53 | #endif -------------------------------------------------------------------------------- /include/fg/render_task_base.hpp: -------------------------------------------------------------------------------- 1 | #ifndef FG_RENDER_TASK_BASE_HPP_ 2 | #define FG_RENDER_TASK_BASE_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace fg 9 | { 10 | class framegraph; 11 | class render_task_builder; 12 | class resource_base; 13 | 14 | class render_task_base 15 | { 16 | public: 17 | explicit render_task_base (const std::string& name) : name_(name), cull_immune_(false), ref_count_(0) 18 | { 19 | 20 | } 21 | render_task_base (const render_task_base& that) = delete ; 22 | render_task_base ( render_task_base&& temp) = default; 23 | virtual ~render_task_base () = default; 24 | render_task_base& operator=(const render_task_base& that) = delete ; 25 | render_task_base& operator=( render_task_base&& temp) = default; 26 | 27 | const std::string& name () const 28 | { 29 | return name_; 30 | } 31 | void set_name (const std::string& name) 32 | { 33 | name_ = name; 34 | } 35 | 36 | bool cull_immune () const 37 | { 38 | return cull_immune_; 39 | } 40 | void set_cull_immune (const bool cull_immune) 41 | { 42 | cull_immune_ = cull_immune; 43 | } 44 | 45 | protected: 46 | friend framegraph; 47 | friend render_task_builder; 48 | 49 | virtual void setup (render_task_builder& builder) = 0; 50 | virtual void execute() const = 0; 51 | 52 | std::string name_ ; 53 | bool cull_immune_; 54 | std::vector creates_ ; 55 | std::vector reads_ ; 56 | std::vector writes_ ; 57 | std::size_t ref_count_ ; // Computed through framegraph compilation. 58 | }; 59 | } 60 | 61 | #endif -------------------------------------------------------------------------------- /include/fg/resource_base.hpp: -------------------------------------------------------------------------------- 1 | #ifndef FG_RESOURCE_BASE_HPP_ 2 | #define FG_RESOURCE_BASE_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace fg 9 | { 10 | class framegraph; 11 | class render_task_base; 12 | class render_task_builder; 13 | 14 | class resource_base 15 | { 16 | public: 17 | explicit resource_base (const std::string& name, const render_task_base* creator) : name_(name), creator_(creator), ref_count_(0) 18 | { 19 | static std::size_t id = 0; 20 | id_ = id++; 21 | } 22 | resource_base (const resource_base& that) = delete ; 23 | resource_base ( resource_base&& temp) = default; 24 | virtual ~resource_base () = default; 25 | resource_base& operator=(const resource_base& that) = delete ; 26 | resource_base& operator=( resource_base&& temp) = default; 27 | 28 | std::size_t id () const 29 | { 30 | return id_; 31 | } 32 | 33 | const std::string& name () const 34 | { 35 | return name_; 36 | } 37 | void set_name (const std::string& name) 38 | { 39 | name_ = name; 40 | } 41 | 42 | bool transient() const 43 | { 44 | return creator_ != nullptr; 45 | } 46 | 47 | protected: 48 | friend framegraph; 49 | friend render_task_builder; 50 | 51 | virtual void realize () = 0; 52 | virtual void derealize() = 0; 53 | 54 | std::size_t id_ ; 55 | std::string name_ ; 56 | const render_task_base* creator_ ; 57 | std::vector readers_ ; 58 | std::vector writers_ ; 59 | std::size_t ref_count_; // Computed through framegraph compilation. 60 | }; 61 | } 62 | 63 | #endif -------------------------------------------------------------------------------- /include/fg/resource.hpp: -------------------------------------------------------------------------------- 1 | #ifndef FG_RESOURCE_HPP_ 2 | #define FG_RESOURCE_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | namespace fg 12 | { 13 | class render_task_base; 14 | 15 | template 16 | class resource : public resource_base 17 | { 18 | public: 19 | using description_type = description_type_; 20 | using actual_type = actual_type_ ; 21 | 22 | explicit resource (const std::string& name, const render_task_base* creator, const description_type& description) 23 | : resource_base(name, creator), description_(description), actual_(std::unique_ptr()) 24 | { 25 | // Transient (normal) constructor. 26 | } 27 | explicit resource (const std::string& name, const description_type& description, actual_type* actual = nullptr) 28 | : resource_base(name, nullptr), description_(description), actual_(actual) 29 | { 30 | // Retained (import) constructor. 31 | if(!actual) actual_ = fg::realize(description_); 32 | } 33 | resource (const resource& that) = delete ; 34 | resource ( resource&& temp) = default; 35 | ~resource () = default; 36 | resource& operator=(const resource& that) = delete ; 37 | resource& operator=( resource&& temp) = default; 38 | 39 | const description_type& description () const 40 | { 41 | return description_; 42 | } 43 | actual_type* actual () const // If transient, only valid through the realized interval of the resource. 44 | { 45 | return std::holds_alternative>(actual_) ? std::get>(actual_).get() : std::get(actual_); 46 | } 47 | 48 | protected: 49 | void realize () override 50 | { 51 | if (transient()) std::get>(actual_) = fg::realize(description_); 52 | } 53 | void derealize() override 54 | { 55 | if (transient()) std::get>(actual_).reset(); 56 | } 57 | 58 | description_type description_; 59 | std::variant, actual_type*> actual_ ; 60 | }; 61 | } 62 | 63 | #endif -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Concise header-only library cmake template. Configuring this template into an actual project: 2 | # - Add your source files to the sources list. 3 | # - Add your third party libraries via the import_library function. 4 | # - Add your test source files to the test sources list (optional). 5 | 6 | ################################################## Project ################################################## 7 | cmake_minimum_required(VERSION 3.2 FATAL_ERROR) 8 | project (fg VERSION 1.0 LANGUAGES CXX) 9 | list (APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") 10 | set_property (GLOBAL PROPERTY USE_FOLDERS ON) 11 | 12 | ################################################## Options ################################################## 13 | option(BUILD_TESTS "Build tests." OFF) 14 | 15 | ################################################## Sources ################################################## 16 | set(PROJECT_SOURCES 17 | CMakeLists.txt 18 | cmake/assign_source_group.cmake 19 | cmake/import_library.cmake 20 | 21 | include/fg/framegraph.hpp 22 | include/fg/realize.hpp 23 | include/fg/render_task.hpp 24 | include/fg/render_task_base.hpp 25 | include/fg/render_task_builder.hpp 26 | include/fg/resource.hpp 27 | include/fg/resource_base.hpp 28 | 29 | license.txt 30 | readme.md 31 | ) 32 | include(assign_source_group) 33 | assign_source_group(${PROJECT_SOURCES}) 34 | 35 | ################################################## Dependencies ################################################## 36 | include(import_library) 37 | 38 | ################################################## Targets ################################################## 39 | add_library(${PROJECT_NAME} INTERFACE) 40 | target_include_directories(${PROJECT_NAME} INTERFACE 41 | $ 42 | $ 43 | $) 44 | target_include_directories(${PROJECT_NAME} INTERFACE ${PROJECT_INCLUDE_DIRS}) 45 | target_link_libraries (${PROJECT_NAME} INTERFACE ${PROJECT_LIBRARIES}) 46 | 47 | # Hack for header-only project to appear in the IDEs. 48 | add_library(${PROJECT_NAME}_ STATIC ${PROJECT_SOURCES}) 49 | target_include_directories(${PROJECT_NAME}_ 50 | PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_BINARY_DIR}) 51 | target_include_directories(${PROJECT_NAME}_ PUBLIC ${PROJECT_INCLUDE_DIRS}) 52 | target_link_libraries (${PROJECT_NAME}_ PUBLIC ${PROJECT_LIBRARIES}) 53 | set_target_properties (${PROJECT_NAME}_ PROPERTIES LINKER_LANGUAGE CXX) 54 | set_property (TARGET ${PROJECT_NAME}_ PROPERTY CXX_STANDARD 17) 55 | set_property (TARGET ${PROJECT_NAME}_ PROPERTY CXX_STANDARD_REQUIRED ON) 56 | 57 | ################################################## Testing ################################################## 58 | if(BUILD_TESTS) 59 | enable_testing() 60 | 61 | set(PROJECT_TEST_SOURCES 62 | tests/framegraph_test.cpp 63 | ) 64 | 65 | foreach(_SOURCE ${PROJECT_TEST_SOURCES}) 66 | get_filename_component(_NAME ${_SOURCE} NAME_WE) 67 | set (_SOURCES tests/catch.hpp tests/main.cpp ${_SOURCE}) 68 | add_executable (${_NAME} ${_SOURCES}) 69 | target_link_libraries (${_NAME} ${PROJECT_NAME}) 70 | add_test (${_NAME} ${_NAME}) 71 | set_property (TARGET ${_NAME} PROPERTY FOLDER "Tests") 72 | set_property (TARGET ${_NAME} PROPERTY CXX_STANDARD 17) 73 | set_property (TARGET ${_NAME} PROPERTY CXX_STANDARD_REQUIRED ON) 74 | source_group ("source" FILES ${_SOURCES}) 75 | endforeach() 76 | endif() 77 | 78 | ################################################## Installation ################################################## 79 | install(TARGETS ${PROJECT_NAME} EXPORT "${PROJECT_NAME}-config") 80 | install(DIRECTORY include/ DESTINATION include) 81 | install(EXPORT "${PROJECT_NAME}-config" DESTINATION "cmake") 82 | export (TARGETS "${PROJECT_NAME}" FILE "${PROJECT_NAME}-config.cmake") 83 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | **What is a framegraph?** 2 | 3 | A rendering abstraction which describes a frame as a directed acyclic graph of render tasks and resources. 4 | Based on the [Game Developers Conference (GDC) presentation by Yuriy O’Donnell](https://www.gdcvault.com/play/1024612/FrameGraph-Extensible-Rendering-Architecture-in). 5 | 6 | **What is a render task?** 7 | 8 | A compute or graphics task to be performed as part of a rendering pipeline. 9 | 10 | **What is a resource?** 11 | 12 | Data created, read or written by a render task. Alternates between two states; virtual and real. 13 | While virtual, the resource is not instantiated but contains the necessary information to do so. 14 | While real, the resource is instantiated and ready for use. 15 | A **transient** resource is owned, realized and virtualized by the framegraph. 16 | A **retained** resource is always real and is imported into the framegraph. 17 | 18 | **Usage** 19 | 20 | First, create descriptions for your rendering resources (e.g. buffers, textures) and declare them as framegraph resources. 21 | ```cxx 22 | struct buffer_description 23 | { 24 | std::size_t size; 25 | }; 26 | struct texture_description 27 | { 28 | std::size_t levels; 29 | GLenum format; 30 | std::array size ; 31 | }; 32 | 33 | using buffer_resource = fg::resource; 34 | using texture_1d_resource = fg::resource; 35 | using texture_2d_resource = fg::resource; 36 | using texture_3d_resource = fg::resource; 37 | ``` 38 | 39 | Then, specialize `fg::realize` for each declared resource. This function takes in a resource description and returns an actual resource. 40 | ```cxx 41 | namespace fg 42 | { 43 | template<> 44 | std::unique_ptr realize(const buffer_description& description) 45 | { 46 | auto actual = std::make_unique(); 47 | actual->set_size(static_cast(description.size)); 48 | return actual; 49 | } 50 | template<> 51 | std::unique_ptr realize(const texture_description& description) 52 | { 53 | auto actual = std::make_unique(); 54 | actual->set_storage( 55 | description.levels , 56 | description.format , 57 | description.size[0], 58 | description.size[1]); 59 | return actual; 60 | } 61 | } 62 | ``` 63 | 64 | You are now ready to create a framegraph and add your render tasks / retained resources to it. 65 | ```cxx 66 | fg::framegraph framegraph; 67 | 68 | gl::texture_2d backbuffer; 69 | auto retained_resource = framegraph.add_retained_resource( 70 | "Backbuffer", 71 | texture_description(), 72 | &backbuffer); 73 | 74 | struct render_task_data 75 | { 76 | texture_2d_resource* input1; 77 | texture_2d_resource* input2; 78 | texture_2d_resource* input3; 79 | texture_2d_resource* output; 80 | }; 81 | auto render_task = framegraph.add_render_task( 82 | "Render Task", 83 | [&] (render_task_data& data, fg::render_task_builder& builder) 84 | { 85 | data.input1 = builder.create("Texture 1", texture_description()); 86 | data.input2 = builder.create("Texture 2", texture_description()); 87 | data.input3 = builder.create("Texture 3", texture_description()); 88 | data.output = builder.write (retained_resource); 89 | }, 90 | [=] (const render_task_data& data) 91 | { 92 | auto actual1 = data.input1->actual(); 93 | auto actual2 = data.input2->actual(); 94 | auto actual3 = data.input3->actual(); 95 | auto actual4 = data.output->actual(); 96 | // Perform actual rendering. You may load resources from CPU by capturing them. 97 | }); 98 | 99 | auto& data = render_task->data(); 100 | ``` 101 | 102 | Once all render tasks and resources are added, call `framegraph.compile()`. Then, `framegraph.execute()` in each update. It is also possible to export to GraphViz for debugging / visualization via `framegraph.export_graphviz(filename)`: 103 | 104 | ![alt text](https://github.com/acdemiralp/fg/blob/develop/docs/images/example.png "Example 1") 105 | 106 | ![alt text](https://github.com/acdemiralp/fg/blob/develop/docs/images/example2.png "Example 2") 107 | 108 | **Next Steps** 109 | - Asynchronous render tasks (+ resource / aliasing barriers). 110 | -------------------------------------------------------------------------------- /tests/framegraph_test.cpp: -------------------------------------------------------------------------------- 1 | #include "catch.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | namespace gl 9 | { 10 | using buffer = std::size_t; 11 | using texture_1d = std::size_t; 12 | using texture_2d = std::size_t; 13 | using texture_3d = std::size_t; 14 | } 15 | 16 | namespace glr 17 | { 18 | struct buffer_description 19 | { 20 | std::size_t size; 21 | }; 22 | struct texture_description 23 | { 24 | std::size_t levels; 25 | std::size_t format; 26 | std::array size ; 27 | }; 28 | 29 | using buffer_resource = fg::resource; 30 | using texture_1d_resource = fg::resource; 31 | using texture_2d_resource = fg::resource; 32 | using texture_3d_resource = fg::resource; 33 | } 34 | 35 | namespace fg 36 | { 37 | template<> 38 | std::unique_ptr realize(const glr::buffer_description& description) 39 | { 40 | return std::make_unique(description.size); 41 | } 42 | template<> 43 | std::unique_ptr realize(const glr::texture_description& description) 44 | { 45 | return std::make_unique(description.levels); 46 | } 47 | } 48 | 49 | TEST_CASE("Framegraph test.", "[framegraph]") 50 | { 51 | fg::framegraph framegraph; 52 | 53 | auto retained_resource = framegraph.add_retained_resource("Retained Resource 1", glr::texture_description(), static_cast(nullptr)); 54 | 55 | // First render task declaration. 56 | struct render_task_1_data 57 | { 58 | glr::texture_2d_resource* output1; 59 | glr::texture_2d_resource* output2; 60 | glr::texture_2d_resource* output3; 61 | glr::texture_2d_resource* output4; 62 | }; 63 | auto render_task_1 = framegraph.add_render_task( 64 | "Render Task 1", 65 | [&] (render_task_1_data& data, fg::render_task_builder& builder) 66 | { 67 | data.output1 = builder.create("Resource 1", glr::texture_description()); 68 | data.output2 = builder.create("Resource 2", glr::texture_description()); 69 | data.output3 = builder.create("Resource 3", glr::texture_description()); 70 | data.output4 = builder.write (retained_resource); 71 | }, 72 | [=] (const render_task_1_data& data) 73 | { 74 | // Perform actual rendering. You may load resources from CPU by capturing them. 75 | auto actual1 = data.output1->actual(); 76 | auto actual2 = data.output2->actual(); 77 | auto actual3 = data.output3->actual(); 78 | auto actual4 = data.output4->actual(); 79 | }); 80 | 81 | auto& data_1 = render_task_1->data(); 82 | REQUIRE(data_1.output1->id() == 1); 83 | REQUIRE(data_1.output2->id() == 2); 84 | REQUIRE(data_1.output3->id() == 3); 85 | 86 | // Second render pass declaration. 87 | struct render_task_2_data 88 | { 89 | glr::texture_2d_resource* input1; 90 | glr::texture_2d_resource* input2; 91 | glr::texture_2d_resource* output1; 92 | glr::texture_2d_resource* output2; 93 | }; 94 | auto render_task_2 = framegraph.add_render_task( 95 | "Render Task 2", 96 | [&] (render_task_2_data& data, fg::render_task_builder& builder) 97 | { 98 | data.input1 = builder.read (data_1.output1); 99 | data.input2 = builder.read (data_1.output2); 100 | data.output1 = builder.write (data_1.output3); 101 | data.output2 = builder.create("Resource 4", glr::texture_description()); 102 | }, 103 | [=] (const render_task_2_data& data) 104 | { 105 | // Perform actual rendering. You may load resources from CPU by capturing them. 106 | auto actual1 = data.input1 ->actual(); 107 | auto actual2 = data.input2 ->actual(); 108 | auto actual3 = data.output1->actual(); 109 | auto actual4 = data.output2->actual(); 110 | }); 111 | 112 | auto& data_2 = render_task_2->data(); 113 | REQUIRE(data_2.output2->id() == 4); 114 | 115 | struct render_task_3_data 116 | { 117 | glr::texture_2d_resource* input1; 118 | glr::texture_2d_resource* input2; 119 | glr::texture_2d_resource* output; 120 | }; 121 | auto render_task_3 = framegraph.add_render_task( 122 | "Render Task 3", 123 | [&] (render_task_3_data& data, fg::render_task_builder& builder) 124 | { 125 | data.input1 = builder.read (data_2.output1); 126 | data.input2 = builder.read (data_2.output2); 127 | data.output = builder.write(retained_resource); 128 | }, 129 | [=] (const render_task_3_data& data) 130 | { 131 | // Perform actual rendering. You may load resources from CPU by capturing them. 132 | auto actual1 = data.input1->actual(); 133 | auto actual2 = data.input2->actual(); 134 | auto actual3 = data.output->actual(); 135 | }); 136 | 137 | framegraph.compile (); 138 | for(auto i = 0; i < 100; i++) 139 | framegraph.execute (); 140 | framegraph.export_graphviz("framegraph.gv"); 141 | framegraph.clear (); 142 | } -------------------------------------------------------------------------------- /include/fg/framegraph.hpp: -------------------------------------------------------------------------------- 1 | #ifndef FG_FRAMEGRAPH_HPP_ 2 | #define FG_FRAMEGRAPH_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | namespace fg 18 | { 19 | class framegraph 20 | { 21 | public: 22 | framegraph () = default; 23 | framegraph (const framegraph& that) = delete ; 24 | framegraph ( framegraph&& temp) = default; 25 | virtual ~framegraph () = default; 26 | framegraph& operator=(const framegraph& that) = delete ; 27 | framegraph& operator=( framegraph&& temp) = default; 28 | 29 | template 30 | render_task* add_render_task (argument_types&&... arguments) 31 | { 32 | render_tasks_.emplace_back(std::make_unique>(arguments...)); 33 | auto render_task = render_tasks_.back().get(); 34 | 35 | render_task_builder builder(this, render_task); 36 | render_task->setup(builder); 37 | 38 | return static_cast*>(render_task); 39 | } 40 | template 41 | resource* add_retained_resource(const std::string& name, const description_type& description, actual_type* actual = nullptr) 42 | { 43 | resources_.emplace_back(std::make_unique>(name, description, actual)); 44 | return static_cast*>(resources_.back().get()); 45 | } 46 | void compile () 47 | { 48 | // Reference counting. 49 | for (auto& render_task : render_tasks_) 50 | render_task->ref_count_ = render_task->creates_.size() + render_task->writes_.size(); 51 | for (auto& resource : resources_ ) 52 | resource ->ref_count_ = resource ->readers_.size(); 53 | 54 | // Culling via flood fill from unreferenced resources. 55 | std::stack unreferenced_resources; 56 | for (auto& resource : resources_) 57 | if (resource->ref_count_ == 0 && resource->transient()) 58 | unreferenced_resources.push(resource.get()); 59 | while (!unreferenced_resources.empty()) 60 | { 61 | auto unreferenced_resource = unreferenced_resources.top(); 62 | unreferenced_resources.pop(); 63 | 64 | auto creator = const_cast(unreferenced_resource->creator_); 65 | if (creator->ref_count_ > 0) 66 | creator->ref_count_--; 67 | if (creator->ref_count_ == 0 && !creator->cull_immune()) 68 | { 69 | for (auto iteratee : creator->reads_) 70 | { 71 | auto read_resource = const_cast(iteratee); 72 | if (read_resource->ref_count_ > 0) 73 | read_resource->ref_count_--; 74 | if (read_resource->ref_count_ == 0 && read_resource->transient()) 75 | unreferenced_resources.push(read_resource); 76 | } 77 | } 78 | 79 | for(auto c_writer : unreferenced_resource->writers_) 80 | { 81 | auto writer = const_cast(c_writer); 82 | if (writer->ref_count_ > 0) 83 | writer->ref_count_--; 84 | if (writer->ref_count_ == 0 && !writer->cull_immune()) 85 | { 86 | for (auto iteratee : writer->reads_) 87 | { 88 | auto read_resource = const_cast(iteratee); 89 | if (read_resource->ref_count_ > 0) 90 | read_resource->ref_count_--; 91 | if (read_resource->ref_count_ == 0 && read_resource->transient()) 92 | unreferenced_resources.push(read_resource); 93 | } 94 | } 95 | } 96 | } 97 | 98 | // Timeline computation. 99 | timeline_.clear(); 100 | for (auto& render_task : render_tasks_) 101 | { 102 | if (render_task->ref_count_ == 0 && !render_task->cull_immune()) 103 | continue; 104 | 105 | std::vector realized_resources, derealized_resources; 106 | 107 | for (auto resource : render_task->creates_) 108 | { 109 | realized_resources.push_back(const_cast(resource)); 110 | if (resource->readers_.empty() && resource->writers_.empty()) 111 | derealized_resources.push_back(const_cast(resource)); 112 | } 113 | 114 | auto reads_writes = render_task->reads_; 115 | reads_writes.insert(reads_writes.end(), render_task->writes_.begin(), render_task->writes_.end()); 116 | for (auto resource : reads_writes) 117 | { 118 | if (!resource->transient()) 119 | continue; 120 | 121 | auto valid = false; 122 | std::size_t last_index; 123 | if (!resource->readers_.empty()) 124 | { 125 | auto last_reader = std::find_if( 126 | render_tasks_.begin(), 127 | render_tasks_.end (), 128 | [&resource] (const std::unique_ptr& iteratee) 129 | { 130 | return iteratee.get() == resource->readers_.back(); 131 | }); 132 | if(last_reader != render_tasks_.end()) 133 | { 134 | valid = true; 135 | last_index = std::distance(render_tasks_.begin(), last_reader); 136 | } 137 | } 138 | if (!resource->writers_.empty()) 139 | { 140 | auto last_writer = std::find_if( 141 | render_tasks_.begin(), 142 | render_tasks_.end (), 143 | [&resource] (const std::unique_ptr& iteratee) 144 | { 145 | return iteratee.get() == resource->writers_.back(); 146 | }); 147 | if (last_writer != render_tasks_.end()) 148 | { 149 | valid = true; 150 | last_index = std::max(last_index, std::size_t(std::distance(render_tasks_.begin(), last_writer))); 151 | } 152 | } 153 | 154 | if (valid && render_tasks_[last_index] == render_task) 155 | derealized_resources.push_back(const_cast(resource)); 156 | } 157 | 158 | timeline_.push_back(step{render_task.get(), realized_resources, derealized_resources}); 159 | } 160 | } 161 | void execute () const 162 | { 163 | for(auto& step : timeline_) 164 | { 165 | for (auto resource : step.realized_resources ) resource->realize (); 166 | step.render_task->execute(); 167 | for (auto resource : step.derealized_resources) resource->derealize(); 168 | } 169 | } 170 | void clear () 171 | { 172 | render_tasks_.clear(); 173 | resources_ .clear(); 174 | } 175 | void export_graphviz (const std::string& filepath) 176 | { 177 | std::ofstream stream(filepath); 178 | stream << "digraph framegraph \n{\n"; 179 | 180 | stream << "rankdir = LR\n"; 181 | stream << "bgcolor = black\n\n"; 182 | stream << "node [shape=rectangle, fontname=\"helvetica\", fontsize=12]\n\n"; 183 | 184 | for (auto& render_task : render_tasks_) 185 | stream << "\"" << render_task->name() << "\" [label=\"" << render_task->name() << "\\nRefs: " << render_task->ref_count_ << "\", style=filled, fillcolor=darkorange]\n"; 186 | stream << "\n"; 187 | 188 | for (auto& resource : resources_ ) 189 | stream << "\"" << resource ->name() << "\" [label=\"" << resource ->name() << "\\nRefs: " << resource ->ref_count_ << "\\nID: " << resource->id() << "\", style=filled, fillcolor= " << (resource->transient() ? "skyblue" : "steelblue") << "]\n"; 190 | stream << "\n"; 191 | 192 | for (auto& render_task : render_tasks_) 193 | { 194 | stream << "\"" << render_task->name() << "\" -> { "; 195 | for (auto& resource : render_task->creates_) 196 | stream << "\"" << resource->name() << "\" "; 197 | stream << "} [color=seagreen]\n"; 198 | 199 | stream << "\"" << render_task->name() << "\" -> { "; 200 | for (auto& resource : render_task->writes_) 201 | stream << "\"" << resource->name() << "\" "; 202 | stream << "} [color=gold]\n"; 203 | } 204 | stream << "\n"; 205 | 206 | for (auto& resource : resources_) 207 | { 208 | stream << "\"" << resource->name() << "\" -> { "; 209 | for (auto& render_task : resource->readers_) 210 | stream << "\"" << render_task->name() << "\" "; 211 | stream << "} [color=firebrick]\n"; 212 | } 213 | stream << "}"; 214 | } 215 | 216 | protected: 217 | friend render_task_builder; 218 | 219 | struct step 220 | { 221 | render_task_base* render_task ; 222 | std::vector realized_resources ; 223 | std::vector derealized_resources; 224 | }; 225 | 226 | std::vector> render_tasks_; 227 | std::vector> resources_ ; 228 | std::vector timeline_ ; // Computed through framegraph compilation. 229 | }; 230 | 231 | template 232 | resource_type* render_task_builder::create(const std::string& name, const description_type& description) 233 | { 234 | static_assert(std::is_same::value, "Description does not match the resource."); 235 | framegraph_->resources_.emplace_back(std::make_unique(name, render_task_, description)); 236 | const auto resource = framegraph_->resources_.back().get(); 237 | render_task_->creates_.push_back(resource); 238 | return static_cast(resource); 239 | } 240 | template 241 | resource_type* render_task_builder::read (resource_type* resource) 242 | { 243 | resource->readers_.push_back(render_task_); 244 | render_task_->reads_.push_back(resource); 245 | return resource; 246 | } 247 | template 248 | resource_type* render_task_builder::write (resource_type* resource) 249 | { 250 | resource->writers_.push_back(render_task_); 251 | render_task_->writes_.push_back(resource); 252 | return resource; 253 | } 254 | } 255 | 256 | #endif --------------------------------------------------------------------------------