diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
deleted file mode 100644
index 63fda17212cf97f2857ac1891dfad9dd052cbe6a..0000000000000000000000000000000000000000
--- a/.gitlab-ci.yml
+++ /dev/null
@@ -1,125 +0,0 @@
-variables:
-  RUN:
-    value: "all"
-    description: "The tests that should run. Possible values: ubuntu, win-msvc, win-mingw, mac, all."
-  GIT_DEPTH: 15
-
-stages:
-  - build
-  - deploy
-
-build_ubuntu_gcc:
-  only:
-    variables:
-      - $RUN =~ /\bubuntu.*/i || $RUN =~ /\ball.*/i
-  stage: build
-  tags: 
-    - ubuntu-gcc-cached
-  variables:
-    GIT_SUBMODULE_STRATEGY: recursive
-  timeout: 15m
-  retry: 1
-  script:
-    - mkdir debug
-    - cd debug
-    - cmake -DCMAKE_BUILD_TYPE=Debug ..
-    - cmake --build . -j 4
-  artifacts:
-    name: "Documentation - $CI_PIPELINE_ID"
-    paths:
-      - doc/html
-      - doc/latex
-    expire_in: never
-
-build_win10_msvc:
-  only:
-    variables:
-      - $RUN =~ /\bwin-msvc.*/i || $RUN =~ /\ball.*/i
-  stage: build
-  tags: 
-    - win10-msvc-cached
-  variables:
-    GIT_SUBMODULE_STRATEGY: recursive
-  timeout: 15m
-  retry: 0
-  script:
-    - cd 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\'
-    - .\Launch-VsDevShell.ps1
-    - cd $CI_PROJECT_DIR
-    - mkdir debug
-    - cd debug
-    - cmake -DCMAKE_BUILD_TYPE=Debug ..
-    - cmake --build . -j 4
-
-build_win10_mingw:
-  only:
-    variables:
-      - $RUN =~ /\bwin-mingw.*/i || $RUN =~ /\ball.*/i
-  stage: build
-  tags: 
-    - win10-mingw-cached
-  variables:
-    GIT_SUBMODULE_STRATEGY: recursive
-  timeout: 15m
-  retry: 0
-  script:
-    - mkdir debug
-    - cd debug
-    - cmake --no-warn-unused-cli -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Debug -DCMAKE_C_COMPILER:FILEPATH=C:\msys64\mingw64\bin\x86_64-w64-mingw32-gcc.exe -DCMAKE_CXX_COMPILER:FILEPATH=C:\msys64\mingw64\bin\x86_64-w64-mingw32-g++.exe .. -G "Unix Makefiles"
-    - cmake --build . -j 4
-
-build_mac_clang:
-  only:
-    variables:
-      - $RUN =~ /\bmac.*/i || $RUN =~ /\ball.*/i
-  stage: build
-  tags: 
-    - catalina-clang-cached
-  variables:
-    GIT_SUBMODULE_STRATEGY: recursive
-  timeout: 15m
-  retry: 1
-  script:
-    - mkdir debug
-    - cd debug
-    - export LDFLAGS="-L/usr/local/opt/llvm/lib"
-    - export CPPFLAGS="-I/usr/local/opt/llvm/include"
-    - cmake -DCMAKE_C_COMPILER="/usr/local/opt/llvm/bin/clang" -DCMAKE_CXX_COMPILER="/usr/local/opt/llvm/bin/clang++" -DCMAKE_BUILD_TYPE=Debug ..
-    - cmake --build . -j 4
-
-deploy_doc_develop:
-  only:
-    variables:
-      - $RUN =~ /\bubuntu.*/i || $RUN =~ /\ball.*/i
-    refs:
-      - develop
-  stage: deploy
-  needs: ["build_ubuntu_gcc"]
-  dependencies: 
-    - build_ubuntu_gcc
-  tags: 
-    - webserver
-  variables:
-    GIT_STRATEGY: none
-  script:
-    - rsync -avh doc/html/ /var/www/html/develop --delete
-    - echo "Check it out at https://vkcv.de/develop"
-
-deploy_doc_branch:
-  only:
-    variables:
-      - $RUN =~ /\bubuntu.*/i || $RUN =~ /\ball.*/i
-  except:
-    refs:
-      - develop
-  stage: deploy
-  needs: ["build_ubuntu_gcc"]
-  dependencies: 
-    - build_ubuntu_gcc
-  tags: 
-    - webserver
-  variables:
-    GIT_STRATEGY: none
-  script:
-    - rsync -avh  doc/html/ /var/www/html/branch/$CI_COMMIT_BRANCH --delete
-    - echo "Check it out at https://vkcv.de/branch/$CI_COMMIT_BRANCH"
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000000000000000000000000000000000000..3e2cfbfed372f4d7aa182b6129c34f2bc6abccb6
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,43 @@
+# Changelog
+
+## [0.1.0](https://gitlab.uni-koblenz.de/vulkan2021/vkcv-framework/tree/0.1.0) (2021-12-07)
+
+** Platform support:**
+
+ - Linux support (GCC and CLang)
+ - MacOS support (Apple CLang)
+ - Windows support (MSVC and MinGW-GCC experimentally)
+
+** New modules:**
+
+ - [Asset-Loader](modules/asset_loader/README.md): A VkCV module to load basic assets like models, materials and images
+ - [Camera](modules/asset_loader/README.md): A VkCV module to manage cameras and their handle view and projection matrices
+ - [GUI](modules/gui/README.md): A VkCV module to integrate GUI rendering to your application as additional pass
+ - [Material](modules/material/README.md): A VkCV module to abstract typical kinds of materials for rendering
+ - [Meshlet](modules/meshlet/README.md): A VkCV module to divide vertex data of a mesh into meshlets
+ - [Scene](modules/scene/README.md): A VkCV module to load and manage a scene, simplify its rendering and potentially optimize it
+ - [Shader-Compiler](modules/shader_compiler/README.md): A VkCV module to compile shaders at runtime
+ - [Upscaling](modules/upscaling/README.md): A VkCV module to upscale images in realtime
+
+** New features:**
+
+ - Resizable windows
+ - Multiple windows and multiple swapchains (window management)
+ - Dynamically requesting Vulkan features and extensions
+ - Shader reflection and runtime shader compilation (various shader stages)
+ - Realtime ray tracing
+ - Mesh shaders
+ - Indirect dispatch
+ - Compute pipelines and compute shaders
+ - Multiple queues and graphic pipelines
+ - Bindless textures
+ - ImGUI support
+ - Mipmapping
+ - Logging
+ - Command buffer synchronization
+ - Doxygen source code documentation
+ - Buffer, sampler and image management
+ - Camera management with gamepad support
+ - Input event synchronization
+ - Resource management with handles
+
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e3a8d930f061b25631dff84ba8ed425d780f9055..121d499fb2cd337c28524b89ecf1ab9d12607bdf 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -33,8 +33,13 @@ if (CMAKE_BUILD_TYPE)
 	set(vkcv_build_${_vkcv_build_type} 1)
 endif()
 
+if (EXISTS "/usr/bin/mold")
+	set(CMAKE_LINKER "/usr/bin/mold")
+endif()
+
 message(STATUS "Language: [ C++ " ${CMAKE_CXX_STANDARD} " ]")
 message(STATUS "Compiler: [ " ${CMAKE_CXX_COMPILER_ID} " " ${CMAKE_CXX_COMPILER_VERSION} " ]")
+message(STATUS "Linker: [ " ${CMAKE_LINKER} " ]")
 
 if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU") AND (CMAKE_CXX_COMPILER_VERSION VERSION_LESS "9.0.0"))
 	message(FATAL_ERROR "Upgrade your compiler! GCC 9.0+ is required!")
diff --git a/config/Libraries.cmake b/config/Libraries.cmake
index 512669ce85a96f8cc94d8181994cfe458fa8b604..ca2af95ef5533ffaf36cbd439f58cf9bd8e28854 100644
--- a/config/Libraries.cmake
+++ b/config/Libraries.cmake
@@ -16,6 +16,9 @@ if(NOT WIN32)
 	list(APPEND vkcv_flags -fopenmp)
 endif()
 
+# add custom functions to check for git submodules
+include(${vkcv_config_ext}/Git.cmake)
+
 list(APPEND vkcv_definitions _USE_MATH_DEFINES)
 
 # some formatted printing
diff --git a/config/ext/Git.cmake b/config/ext/Git.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..a943ce02ab3a170a67fae7394fa6f616239ab5b4
--- /dev/null
+++ b/config/ext/Git.cmake
@@ -0,0 +1,15 @@
+
+function(use_git_submodule submodule_path submodule_status)
+    file(GLOB path_glob "${submodule_path}/*")
+    list(LENGTH path_glob glob_len)
+
+    if(glob_len GREATER 0)
+        set(${submodule_status} TRUE PARENT_SCOPE)
+        return()
+    endif()
+
+    get_filename_component(submodule_name ${submodule_path} NAME)
+
+    message(WARNING "${submodule_name} is required..! Update the submodules!")
+    set(${submodule_status} FALSE PARENT_SCOPE)
+endfunction()
diff --git a/config/ext/ProjectFix.cmake b/config/ext/ProjectFix.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..d3f26b4a5323f473868d87cc94d39b52eafd4313
--- /dev/null
+++ b/config/ext/ProjectFix.cmake
@@ -0,0 +1,13 @@
+
+# the first argument should be the project's target
+macro(fix_project)
+    # this should fix the execution path to load local files from the project (for MSVC)
+    if(MSVC)
+        set_target_properties(${ARGV1} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+        set_target_properties(${ARGV1} PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+
+        # in addition to setting the output directory, the working directory has to be set
+        # by default visual studio sets the working directory to the build directory, when using the debugger
+        set_target_properties(${ARGV1} PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+    endif()
+endmacro()
diff --git a/config/lib/GLFW.cmake b/config/lib/GLFW.cmake
index 9668694de5b6887c163f74b626c71873e3f611f8..977eabc8c3916da694420eb8b069e058b94c15f2 100644
--- a/config/lib/GLFW.cmake
+++ b/config/lib/GLFW.cmake
@@ -6,14 +6,14 @@ if (glfw3_FOUND)
 
     message(${vkcv_config_msg} " GLFW    -   " ${glfw3_VERSION})
 else()
-    if (EXISTS "${vkcv_lib_path}/glfw")
+    use_git_submodule("${vkcv_lib_path}/glfw" glfw_status)
+
+    if (${glfw_status})
         add_subdirectory(${vkcv_lib}/glfw)
 
         list(APPEND vkcv_libraries glfw)
         list(APPEND vkcv_includes ${vkcv_lib_path}/glfw/include)
 
         message(${vkcv_config_msg} " GLFW    -   " ${glfw3_VERSION})
-    else()
-        message(WARNING "GLFW is required..! Update the submodules!")
     endif ()
 endif ()
diff --git a/config/lib/SPIRV_Cross.cmake b/config/lib/SPIRV_Cross.cmake
index 00ae45527d0f49c5632d71e19509871d437ae691..ce4b2e437467436daa7e1bc04975be4fa9609fef 100644
--- a/config/lib/SPIRV_Cross.cmake
+++ b/config/lib/SPIRV_Cross.cmake
@@ -5,7 +5,9 @@ if (spirv-cross_FOUND)
 
     message(${vkcv_config_msg} " SPIRV Cross    - " ${SPIRV_CROSS_VERSION})
 else()
-    if (EXISTS "${vkcv_lib_path}/SPIRV-Cross")
+    use_git_submodule("${vkcv_lib_path}/SPIRV-Cross" spirv_cross_status)
+
+    if (${spirv_cross_status})
         set(SPIRV_CROSS_EXCEPTIONS_TO_ASSERTIONS OFF CACHE INTERNAL "")
         set(SPIRV_CROSS_SHARED OFF CACHE INTERNAL "")
         set(SPIRV_CROSS_STATIC ON CACHE INTERNAL "")
@@ -28,7 +30,5 @@ else()
         list(APPEND vkcv_includes ${vkcv_lib_path}/SPIV-Cross/include)
 
         message(${vkcv_config_msg} " SPIRV Cross    - " ${SPIRV_CROSS_VERSION})
-    else()
-        message(WARNING "SPIRV-Cross is required..! Update the submodules!")
     endif ()
 endif ()
\ No newline at end of file
diff --git a/config/lib/VulkanMemoryAllocator.cmake b/config/lib/VulkanMemoryAllocator.cmake
index 5f670ff04633e1747accb8f1598fee028a287168..fcde834d920e80f8ba47f8eaea0416127ee0347b 100644
--- a/config/lib/VulkanMemoryAllocator.cmake
+++ b/config/lib/VulkanMemoryAllocator.cmake
@@ -1,6 +1,12 @@
 
-if (EXISTS "${vkcv_lib_path}/VulkanMemoryAllocator-Hpp")
-	set(VMA_HPP_PATH "${vkcv_lib_path}/VulkanMemoryAllocator-Hpp" CACHE INTERNAL "")
+use_git_submodule("${vkcv_lib_path}/VulkanMemoryAllocator-Hpp" vma_hpp_status)
+
+if (${vma_hpp_status})
+	if (EXISTS "${vkcv_lib_path}/VulkanMemoryAllocator-Hpp/include")
+		set(VMA_HPP_PATH "${vkcv_lib_path}/VulkanMemoryAllocator-Hpp/include" CACHE INTERNAL "")
+	else()
+		set(VMA_HPP_PATH "${vkcv_lib_path}/VulkanMemoryAllocator-Hpp" CACHE INTERNAL "")
+	endif()
 	
 	set(VMA_RECORDING_ENABLED OFF CACHE INTERNAL "")
 	set(VMA_USE_STL_CONTAINERS OFF CACHE INTERNAL "")
@@ -14,9 +20,7 @@ if (EXISTS "${vkcv_lib_path}/VulkanMemoryAllocator-Hpp")
 	add_subdirectory(${vkcv_config_lib}/vma)
 	
 	list(APPEND vkcv_libraries VulkanMemoryAllocator)
-	list(APPEND vkcv_includes ${vkcv_lib_path}/VulkanMemoryAllocator-Hpp)
+	list(APPEND vkcv_includes ${VMA_HPP_PATH})
 	
 	message(${vkcv_config_msg} " VMA     - ")
-else()
-	message(WARNING "VulkanMemoryAllocator is required..! Update the submodules!")
 endif ()
diff --git a/include/vkcv/ComputePipelineConfig.hpp b/include/vkcv/ComputePipelineConfig.hpp
index 38e9e6da5db7dc42f4acd408d961f87183f8a1cc..e3157f7a4e688fb2b33397b713cdb93e1cacfcaf 100644
--- a/include/vkcv/ComputePipelineConfig.hpp
+++ b/include/vkcv/ComputePipelineConfig.hpp
@@ -7,13 +7,14 @@
 
 #include <vector>
 
+#include "Handles.hpp"
 #include "ShaderProgram.hpp"
 
 namespace vkcv {
 	
     struct ComputePipelineConfig {
         ShaderProgram&                          m_ShaderProgram;
-        std::vector<vk::DescriptorSetLayout>  	m_DescriptorSetLayouts;
+        std::vector<DescriptorSetLayoutHandle>	m_DescriptorSetLayouts;
     };
 	
 }
\ No newline at end of file
diff --git a/include/vkcv/Core.hpp b/include/vkcv/Core.hpp
index 228513227e42d7c4073a988d522b0fd8da463423..e34b625bf182647a90b8376a66546ef7645aa5c4 100644
--- a/include/vkcv/Core.hpp
+++ b/include/vkcv/Core.hpp
@@ -28,6 +28,9 @@
 #include "DrawcallRecording.hpp"
 #include "CommandRecordingFunctionTypes.hpp"
 
+#define VKCV_FRAMEWORK_NAME "VkCV"
+#define VKCV_FRAMEWORK_VERSION (VK_MAKE_VERSION(0, 1, 0))
+
 namespace vkcv
 {
 
@@ -208,7 +211,7 @@ namespace vkcv
         [[nodiscard]]
         SamplerHandle createSampler(SamplerFilterType magFilter, SamplerFilterType minFilter,
 									SamplerMipmapMode mipmapMode, SamplerAddressMode addressMode,
-									float mipLodBias = 0.0f);
+									float mipLodBias = 0.0f, SamplerBorderColor borderColor = SamplerBorderColor::INT_ZERO_OPAQUE);
 
         /**
          * Creates an #Image with a given format, width, height and depth.
diff --git a/include/vkcv/DrawcallRecording.hpp b/include/vkcv/DrawcallRecording.hpp
index 1bd90354b8fb2a775c791c4866bcc51362a63af1..244508b4c4c2677f608573275b32a25fef435951 100644
--- a/include/vkcv/DrawcallRecording.hpp
+++ b/include/vkcv/DrawcallRecording.hpp
@@ -10,6 +10,7 @@
 #include "Handles.hpp"
 #include "DescriptorConfig.hpp"
 #include "PushConstants.hpp"
+
 #include "Buffer.hpp"
 
 namespace vkcv {
@@ -28,12 +29,12 @@ namespace vkcv {
     };
 
     struct DescriptorSetUsage {
-        inline DescriptorSetUsage(uint32_t setLocation, vk::DescriptorSet vulkanHandle,
+        inline DescriptorSetUsage(uint32_t setLocation, DescriptorSetHandle descriptorSet,
 								  const std::vector<uint32_t>& dynamicOffsets = {}) noexcept
-            : setLocation(setLocation), vulkanHandle(vulkanHandle), dynamicOffsets(dynamicOffsets) {}
+            : setLocation(setLocation), descriptorSet(descriptorSet), dynamicOffsets(dynamicOffsets) {}
 
         const uint32_t          	setLocation;
-        const vk::DescriptorSet 	vulkanHandle;
+        const DescriptorSetHandle 	descriptorSet;
         const std::vector<uint32_t> dynamicOffsets;
     };
 
@@ -59,8 +60,6 @@ namespace vkcv {
 
     };
 
-    vk::IndexType getIndexType(IndexBitCount indexByteCount);
-
     struct DrawcallInfo {
         inline DrawcallInfo(const Mesh& mesh, const std::vector<DescriptorSetUsage>& descriptorSets, const uint32_t instanceCount = 1)
             : mesh(mesh), descriptorSets(descriptorSets), instanceCount(instanceCount){}
@@ -70,22 +69,6 @@ namespace vkcv {
         uint32_t                        instanceCount;
     };
 
-    void recordDrawcall(
-        const DrawcallInfo      &drawcall,
-        vk::CommandBuffer       cmdBuffer,
-        vk::PipelineLayout      pipelineLayout,
-        const PushConstants     &pushConstants,
-        const size_t            drawcallIndex);
-
-    void recordIndirectDrawcall(
-            const DrawcallInfo                                  &drawcall,
-            vk::CommandBuffer                                   cmdBuffer,
-            const vkcv::Buffer<vk::DrawIndexedIndirectCommand>  &drawBuffer,
-            const uint32_t                                      drawCount,
-            vk::PipelineLayout                                  pipelineLayout,
-            const PushConstants                                 &pushConstants,
-            const size_t                                        drawcallIndex);
-
     void InitMeshShaderDrawFunctions(vk::Device device);
 
     struct MeshShaderDrawcall {
@@ -97,9 +80,10 @@ namespace vkcv {
     };
 
     void recordMeshShaderDrawcall(
+		const Core&								core,
         vk::CommandBuffer                       cmdBuffer,
         vk::PipelineLayout                      pipelineLayout,
-        const PushConstants&                 pushConstantData,
+        const PushConstants&                 	pushConstantData,
         const uint32_t                          pushConstantOffset,
         const MeshShaderDrawcall&               drawcall,
         const uint32_t                          firstTask);
diff --git a/include/vkcv/FeatureManager.hpp b/include/vkcv/FeatureManager.hpp
index 136972fda74c0db835fbf43ebb6f4a3db7ab3a0c..0e7fb2a5265a91b961328546aba9ba8f335e73e9 100644
--- a/include/vkcv/FeatureManager.hpp
+++ b/include/vkcv/FeatureManager.hpp
@@ -224,6 +224,12 @@ namespace vkcv {
 		[[nodiscard]]
 		bool checkSupport(const vk::PhysicalDeviceMeshShaderFeaturesNV& features, bool required) const;
 		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceShaderAtomicFloatFeaturesEXT& features, bool required) const;
+		
+		[[nodiscard]]
+		bool checkSupport(const vk::PhysicalDeviceShaderAtomicFloat2FeaturesEXT& features, bool required) const;
+		
 		/**
          * @brief Checks support of the @p vk::PhysicalDeviceVulkan12Features.
          * @param features The features
diff --git a/include/vkcv/GraphicsPipelineConfig.hpp b/include/vkcv/GraphicsPipelineConfig.hpp
index a9f0996b3e8e64d4922a09af930ef6ab093e70c6..3efde5bcbb80cd98df964f1bc7e03798d440ad02 100644
--- a/include/vkcv/GraphicsPipelineConfig.hpp
+++ b/include/vkcv/GraphicsPipelineConfig.hpp
@@ -15,7 +15,7 @@
 
 namespace vkcv {
 
-    enum class PrimitiveTopology{PointList, LineList, TriangleList };
+    enum class PrimitiveTopology{PointList, LineList, TriangleList, PatchList };
 	enum class CullMode{ None, Front, Back };
     enum class DepthTest { None, Less, LessEqual, Greater, GreatherEqual, Equal };
 
@@ -29,7 +29,7 @@ namespace vkcv {
 		uint32_t                              	m_Height;
         PassHandle                            	m_PassHandle;
         VertexLayout                          	m_VertexLayout;
-        std::vector<vk::DescriptorSetLayout>  	m_DescriptorLayouts;
+        std::vector<DescriptorSetLayoutHandle>	m_DescriptorLayouts;
         bool                                  	m_UseDynamicViewport;
         bool                                  	m_UseConservativeRasterization 	= false;
         PrimitiveTopology                     	m_PrimitiveTopology 			= PrimitiveTopology::TriangleList;
@@ -40,6 +40,7 @@ namespace vkcv {
         DepthTest                               m_depthTest                     = DepthTest::LessEqual;
         bool                                    m_depthWrite                    = true;
         bool                                    m_alphaToCoverage               = false;
+		uint32_t								m_tessellationControlPoints		= 0;
     };
 
 }
\ No newline at end of file
diff --git a/include/vkcv/Sampler.hpp b/include/vkcv/Sampler.hpp
index ad2ba8bd6c4769c1d882d940dbde323ef6623365..891a96f869c03f5f4a064116bff603acbf2d47e3 100644
--- a/include/vkcv/Sampler.hpp
+++ b/include/vkcv/Sampler.hpp
@@ -21,7 +21,19 @@ namespace vkcv {
 		REPEAT = 1,
 		MIRRORED_REPEAT = 2,
 		CLAMP_TO_EDGE = 3,
-		MIRROR_CLAMP_TO_EDGE = 4
+		MIRROR_CLAMP_TO_EDGE = 4,
+		CLAMP_TO_BORDER = 5
+	};
+	
+	enum class SamplerBorderColor {
+		INT_ZERO_OPAQUE = 1,
+		INT_ZERO_TRANSPARENT = 2,
+		
+		FLOAT_ZERO_OPAQUE = 3,
+		FLOAT_ZERO_TRANSPARENT = 4,
+		
+		INT_ONE_OPAQUE = 5,
+		FLOAT_ONE_OPAQUE = 6
 	};
 
 }
diff --git a/include/vkcv/ShaderStage.hpp b/include/vkcv/ShaderStage.hpp
index 83446bd0717cd6beaa010aca4673c44f0e294d7d..b964c63fa93e4c9483affa091a2b2aadcd16f903 100644
--- a/include/vkcv/ShaderStage.hpp
+++ b/include/vkcv/ShaderStage.hpp
@@ -8,26 +8,56 @@
 #include <vulkan/vulkan.hpp>
 
 namespace vkcv {
-
+	
 	enum class ShaderStage : VkShaderStageFlags {
-		VERTEX          = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eVertex),
-		TESS_CONTROL    = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eTessellationControl),
-		TESS_EVAL       = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eTessellationEvaluation),
-		GEOMETRY        = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eGeometry),
-		FRAGMENT        = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eFragment),
-		COMPUTE         = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eCompute),
-		TASK            = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eTaskNV),
-		MESH            = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eMeshNV),
-		RAY_GEN          = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eRaygenKHR), // RTX
-		RAY_ANY_HIT         = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eAnyHitKHR), // RTX
-		RAY_CLOSEST_HIT     = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eClosestHitKHR), // RTX
-		RAY_MISS            = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eMissKHR), // RTX
-		RAY_INTERSECTION    = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eIntersectionKHR), // RTX
-		RAY_CALLABLE        = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eCallableKHR) // RTX
+		VERTEX = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eVertex),
+		TESS_CONTROL = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eTessellationControl),
+		TESS_EVAL = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eTessellationEvaluation),
+		GEOMETRY = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eGeometry),
+		FRAGMENT = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eFragment),
+		COMPUTE = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eCompute),
+		TASK = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eTaskNV),
+		MESH = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eMeshNV),
+		RAY_GEN = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eRaygenKHR),
+		RAY_ANY_HIT = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eAnyHitKHR),
+		RAY_CLOSEST_HIT = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eClosestHitKHR),
+		RAY_MISS = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eMissKHR),
+		RAY_INTERSECTION = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eIntersectionKHR),
+		RAY_CALLABLE = static_cast<VkShaderStageFlags>(vk::ShaderStageFlagBits::eCallableKHR)
 	};
 	
 	using ShaderStages = vk::Flags<ShaderStage>;
 	
+}
+
+namespace vk {
+	
+	template<>
+	struct [[maybe_unused]] FlagTraits<vkcv::ShaderStage> {
+		enum : VkFlags {
+			allFlags [[maybe_unused]] = (
+					VkFlags( vkcv::ShaderStage::VERTEX ) |
+					VkFlags( vkcv::ShaderStage::TESS_CONTROL ) |
+					VkFlags( vkcv::ShaderStage::TESS_EVAL ) |
+					VkFlags( vkcv::ShaderStage::GEOMETRY ) |
+					VkFlags( vkcv::ShaderStage::FRAGMENT ) |
+					VkFlags( vkcv::ShaderStage::COMPUTE ) |
+					VkFlags( vkcv::ShaderStage::TASK ) |
+					VkFlags( vkcv::ShaderStage::MESH ) |
+					VkFlags( vkcv::ShaderStage::RAY_GEN ) |
+					VkFlags( vkcv::ShaderStage::RAY_ANY_HIT ) |
+					VkFlags( vkcv::ShaderStage::RAY_CLOSEST_HIT ) |
+					VkFlags( vkcv::ShaderStage::RAY_MISS ) |
+					VkFlags( vkcv::ShaderStage::RAY_INTERSECTION ) |
+					VkFlags( vkcv::ShaderStage::RAY_CALLABLE )
+			)
+		};
+	};
+	
+}
+
+namespace vkcv {
+	
 	constexpr vk::ShaderStageFlags getShaderStageFlags(ShaderStages shaderStages) noexcept {
 		return vk::ShaderStageFlags(static_cast<VkShaderStageFlags>(shaderStages));
 	}
diff --git a/lib/SPIRV-Cross b/lib/SPIRV-Cross
index ff61890722a91e97c44940494be5b6eed0d5ff5b..6a67891418a3f08be63f92726e049dc788e46f5b 160000
--- a/lib/SPIRV-Cross
+++ b/lib/SPIRV-Cross
@@ -1 +1 @@
-Subproject commit ff61890722a91e97c44940494be5b6eed0d5ff5b
+Subproject commit 6a67891418a3f08be63f92726e049dc788e46f5b
diff --git a/lib/VulkanMemoryAllocator-Hpp b/lib/VulkanMemoryAllocator-Hpp
index 3a61240a5354ce56c222969a69825aabb6ba0a21..c6c3c665b6a29ae546bdec60606a3ef0757ea108 160000
--- a/lib/VulkanMemoryAllocator-Hpp
+++ b/lib/VulkanMemoryAllocator-Hpp
@@ -1 +1 @@
-Subproject commit 3a61240a5354ce56c222969a69825aabb6ba0a21
+Subproject commit c6c3c665b6a29ae546bdec60606a3ef0757ea108
diff --git a/lib/glfw b/lib/glfw
index 0e9ec7788b4985a0df698080258e4091d18dcc3b..4cb36872a5fe448c205d0b46f0e8c8b57530cfe0 160000
--- a/lib/glfw
+++ b/lib/glfw
@@ -1 +1 @@
-Subproject commit 0e9ec7788b4985a0df698080258e4091d18dcc3b
+Subproject commit 4cb36872a5fe448c205d0b46f0e8c8b57530cfe0
diff --git a/modules/asset_loader/config/FX_GLTF.cmake b/modules/asset_loader/config/FX_GLTF.cmake
index 37cd162422d8277022067498f5d5ba3e26e2ae1b..4164c2786d9ada53c979b98bf23f973113edfda5 100644
--- a/modules/asset_loader/config/FX_GLTF.cmake
+++ b/modules/asset_loader/config/FX_GLTF.cmake
@@ -1,5 +1,7 @@
 
-if (EXISTS "${vkcv_asset_loader_lib_path}/fx-gltf")
+use_git_submodule("${vkcv_asset_loader_lib_path}/fx-gltf" fx_gltf_status)
+
+if (${fx_gltf_status})
 	set(FX_GLTF_INSTALL OFF CACHE INTERNAL "")
 	set(FX_GLTF_USE_INSTALLED_DEPS OFF CACHE INTERNAL "")
 	set(BUILD_TESTING OFF CACHE INTERNAL "")
@@ -7,6 +9,4 @@ if (EXISTS "${vkcv_asset_loader_lib_path}/fx-gltf")
 	add_subdirectory(${vkcv_asset_loader_lib}/fx-gltf)
 	
 	list(APPEND vkcv_asset_loader_libraries fx-gltf)
-else()
-	message(WARNING "FX-GLTF is required..! Update the submodules!")
 endif ()
diff --git a/modules/asset_loader/config/NLOHMANN_JSON.cmake b/modules/asset_loader/config/NLOHMANN_JSON.cmake
index 018f6a19809fd3e53e6e790a6fe6447348e43c09..4bd7f4475ef1e748e109814d440762e27948f495 100644
--- a/modules/asset_loader/config/NLOHMANN_JSON.cmake
+++ b/modules/asset_loader/config/NLOHMANN_JSON.cmake
@@ -1,10 +1,10 @@
 
-if (EXISTS "${vkcv_asset_loader_lib_path}/json")
+use_git_submodule("${vkcv_asset_loader_lib_path}/json" json_status)
+
+if (${json_status})
 	set(JSON_BuildTests OFF CACHE INTERNAL "")
 	
 	add_subdirectory(${vkcv_asset_loader_lib}/json)
 	
 	list(APPEND vkcv_asset_loader_libraries nlohmann_json::nlohmann_json)
-else()
-	message(WARNING "NLOHMANN_JSON is required..! Update the submodules!")
 endif ()
diff --git a/modules/asset_loader/config/STB.cmake b/modules/asset_loader/config/STB.cmake
index 1287d0a9ddda559e061ddd680bc815e24b3e2075..61b7cc929b2b5ae96fbbbe479f6e9d48d9db5c69 100644
--- a/modules/asset_loader/config/STB.cmake
+++ b/modules/asset_loader/config/STB.cmake
@@ -1,10 +1,10 @@
 
-if (EXISTS "${vkcv_asset_loader_lib_path}/stb")
+use_git_submodule("${vkcv_asset_loader_lib_path}/stb" stb_status)
+
+if (${stb_status})
 	list(APPEND vkcv_asset_loader_includes ${vkcv_asset_loader_lib}/stb)
 	
 	list(APPEND vkcv_asset_loader_definitions STB_IMAGE_IMPLEMENTATION)
 	list(APPEND vkcv_asset_loader_definitions STBI_ONLY_JPEG)
 	list(APPEND vkcv_asset_loader_definitions STBI_ONLY_PNG)
-else()
-	message(WARNING "STB is required..! Update the submodules!")
 endif ()
diff --git a/modules/asset_loader/lib/fx-gltf b/modules/asset_loader/lib/fx-gltf
index f4f18f2017a049a23748c9c9aad42ba2de20bfd5..7766c237ea81c0bb3759e78e5c0f22854843eef8 160000
--- a/modules/asset_loader/lib/fx-gltf
+++ b/modules/asset_loader/lib/fx-gltf
@@ -1 +1 @@
-Subproject commit f4f18f2017a049a23748c9c9aad42ba2de20bfd5
+Subproject commit 7766c237ea81c0bb3759e78e5c0f22854843eef8
diff --git a/modules/asset_loader/lib/json b/modules/asset_loader/lib/json
index 0972f7ff0e651f09a306dba791cc42024b8642c1..4f8fba14066156b73f1189a2b8bd568bde5284c5 160000
--- a/modules/asset_loader/lib/json
+++ b/modules/asset_loader/lib/json
@@ -1 +1 @@
-Subproject commit 0972f7ff0e651f09a306dba791cc42024b8642c1
+Subproject commit 4f8fba14066156b73f1189a2b8bd568bde5284c5
diff --git a/modules/asset_loader/lib/stb b/modules/asset_loader/lib/stb
index c9064e317699d2e495f36ba4f9ac037e88ee371a..af1a5bc352164740c1cc1354942b1c6b72eacb8a 160000
--- a/modules/asset_loader/lib/stb
+++ b/modules/asset_loader/lib/stb
@@ -1 +1 @@
-Subproject commit c9064e317699d2e495f36ba4f9ac037e88ee371a
+Subproject commit af1a5bc352164740c1cc1354942b1c6b72eacb8a
diff --git a/modules/camera/config/GLM.cmake b/modules/camera/config/GLM.cmake
index f256ccade8c4f44a89744bb7875371324cf2369d..53960241b8c147f522afd7afa3bfedb20f4f0761 100644
--- a/modules/camera/config/GLM.cmake
+++ b/modules/camera/config/GLM.cmake
@@ -5,13 +5,13 @@ if (glm_FOUND)
     list(APPEND vkcv_camera_includes ${GLM_INCLUDE_DIRS})
     list(APPEND vkcv_camera_libraries glm)
 else()
-    if (EXISTS "${vkcv_camera_lib_path}/glm")
+    use_git_submodule("${vkcv_camera_lib_path}/glm" glm_status)
+
+    if (${glm_status})
         add_subdirectory(${vkcv_camera_lib}/glm)
 
         list(APPEND vkcv_camera_includes ${vkcv_camera_lib_path}/glm)
         list(APPEND vkcv_camera_libraries glm)
-    else()
-        message(WARNING "GLM is required..! Update the submodules!")
     endif ()
 endif ()
 
diff --git a/modules/camera/lib/glm b/modules/camera/lib/glm
index 66062497b104ca7c297321bd0e970869b1e6ece5..6ad79aae3eb5bf809c30bf1168171e9e55857e45 160000
--- a/modules/camera/lib/glm
+++ b/modules/camera/lib/glm
@@ -1 +1 @@
-Subproject commit 66062497b104ca7c297321bd0e970869b1e6ece5
+Subproject commit 6ad79aae3eb5bf809c30bf1168171e9e55857e45
diff --git a/modules/gui/config/ImGui.cmake b/modules/gui/config/ImGui.cmake
index 90cdafdeee355af9e63723632572799e135b04da..624ea7701e245ab8c20731d2b226896667c18f89 100644
--- a/modules/gui/config/ImGui.cmake
+++ b/modules/gui/config/ImGui.cmake
@@ -1,5 +1,7 @@
 
-if (EXISTS "${vkcv_gui_lib_path}/imgui")
+use_git_submodule("${vkcv_gui_lib_path}/imgui" imgui_status)
+
+if (${imgui_status})
 	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/backends/imgui_impl_glfw.cpp)
 	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/backends/imgui_impl_glfw.h)
 	list(APPEND vkcv_imgui_sources ${vkcv_gui_lib_path}/imgui/backends/imgui_impl_vulkan.cpp)
@@ -22,6 +24,4 @@ if (EXISTS "${vkcv_gui_lib_path}/imgui")
 	list(APPEND vkcv_gui_include ${vkcv_gui_lib})
 	
 	list(APPEND vkcv_gui_defines IMGUI_DISABLE_WIN32_FUNCTIONS=1)
-else()
-	message(WARNING "IMGUI is required..! Update the submodules!")
 endif ()
diff --git a/modules/gui/lib/imgui b/modules/gui/lib/imgui
index d5828cd988db525f27128edeadb1a689cd2d7461..eda7792b151d138e15df951578253b821ceed5a3 160000
--- a/modules/gui/lib/imgui
+++ b/modules/gui/lib/imgui
@@ -1 +1 @@
-Subproject commit d5828cd988db525f27128edeadb1a689cd2d7461
+Subproject commit eda7792b151d138e15df951578253b821ceed5a3
diff --git a/modules/scene/src/vkcv/scene/MeshPart.cpp b/modules/scene/src/vkcv/scene/MeshPart.cpp
index 437cd44534cee0066e3b9b4fdab924ea9cde7ca8..f9c87f0a0587bd8a897e42d5b8536fe07c61c749 100644
--- a/modules/scene/src/vkcv/scene/MeshPart.cpp
+++ b/modules/scene/src/vkcv/scene/MeshPart.cpp
@@ -70,11 +70,10 @@ namespace vkcv::scene {
 		
 		if (*this) {
 			const auto& material = getMaterial();
-			const auto& descriptorSet = core.getDescriptorSet(material.getDescriptorSet());
 			
 			drawcalls.push_back(DrawcallInfo(
 					vkcv::Mesh(m_vertexBindings, indexBuffer.getVulkanHandle(), m_indexCount),
-					{ DescriptorSetUsage(0, descriptorSet.vulkanHandle) }
+					{ DescriptorSetUsage(0, material.getDescriptorSet()) }
 			));
 		}
 	}
diff --git a/modules/shader_compiler/config/GLSLANG.cmake b/modules/shader_compiler/config/GLSLANG.cmake
index 50b9fd46bd0db9421c632aa0b80fb8df7e3f2123..98bd45497f9d7ed5196dbed486921f8e6ada12da 100644
--- a/modules/shader_compiler/config/GLSLANG.cmake
+++ b/modules/shader_compiler/config/GLSLANG.cmake
@@ -1,5 +1,7 @@
 
-if (EXISTS "${vkcv_shader_compiler_lib_path}/glslang")
+use_git_submodule("${vkcv_shader_compiler_lib_path}/glslang" glslang_status)
+
+if (${glslang_status})
 	set(SKIP_GLSLANG_INSTALL ON CACHE INTERNAL "")
 	set(ENABLE_SPVREMAPPER OFF CACHE INTERNAL "")
 	set(ENABLE_GLSLANG_BINARIES OFF CACHE INTERNAL "")
@@ -23,6 +25,4 @@ if (EXISTS "${vkcv_shader_compiler_lib_path}/glslang")
 	
 	list(APPEND vkcv_shader_compiler_libraries glslang SPIRV)
 	list(APPEND vkcv_shader_compiler_includes ${vkcv_shader_compiler_lib})
-else()
-	message(WARNING "GLSLANG is required..! Update the submodules!")
 endif ()
diff --git a/modules/shader_compiler/include/vkcv/shader/Compiler.hpp b/modules/shader_compiler/include/vkcv/shader/Compiler.hpp
index ddaaf3d995397dbf7b6b25e2ddd675c5d52a1b47..937c43ef58227d6c2ed29a7ac66681f9afadd025 100644
--- a/modules/shader_compiler/include/vkcv/shader/Compiler.hpp
+++ b/modules/shader_compiler/include/vkcv/shader/Compiler.hpp
@@ -6,6 +6,7 @@
 
 #include <vkcv/Event.hpp>
 #include <vkcv/ShaderStage.hpp>
+#include <vkcv/ShaderProgram.hpp>
 
 namespace vkcv::shader {
 
@@ -19,8 +20,13 @@ namespace vkcv::shader {
      * An event function type to be called on compilation completion.
      */
 	typedef typename event_function<ShaderStage, const std::filesystem::path&>::type ShaderCompiledFunction;
-
-    /**
+	
+	/**
+     * An event function type to be called on program compilation completion.
+     */
+	typedef typename event_function<ShaderProgram&>::type ShaderProgramCompiledFunction;
+	
+	/**
      * An abstract class to handle runtime shader compilation.
      */
 	class Compiler {
@@ -58,8 +64,23 @@ namespace vkcv::shader {
 		virtual void compile(ShaderStage shaderStage, const std::filesystem::path& shaderPath,
 							 const ShaderCompiledFunction& compiled,
 							 const std::filesystem::path& includePath, bool update) = 0;
-
-        /**
+		
+		/**
+         * Compile a shader program from a specific map of given file paths for
+         * target pipeline stages with a custom shader include path and an event
+         * function called if the compilation completes.
+         * @param[in,out] program Shader program
+         * @param[in] stages Shader pipeline stages
+         * @param[in] compiled Shader program compilation event
+         * @param[in] includePath Include path for shaders
+         * @param[in] update Flag to update shaders during runtime
+         */
+		void compileProgram(ShaderProgram& program,
+							const std::unordered_map<ShaderStage, const std::filesystem::path>& stages,
+							const ShaderProgramCompiledFunction& compiled,
+							const std::filesystem::path& includePath = "", bool update = false);
+		
+		/**
          * Return the definition value of a macro for shader compilation.
          * @param[in] name Macro definition name
          * @return Macro definition value
diff --git a/modules/shader_compiler/lib/glslang b/modules/shader_compiler/lib/glslang
index fe15158676657bf965e41c32e15ae5db7ea2ab6a..48fd6c82b3fefb38e59dd799d8b12fddddf8e63c 160000
--- a/modules/shader_compiler/lib/glslang
+++ b/modules/shader_compiler/lib/glslang
@@ -1 +1 @@
-Subproject commit fe15158676657bf965e41c32e15ae5db7ea2ab6a
+Subproject commit 48fd6c82b3fefb38e59dd799d8b12fddddf8e63c
diff --git a/modules/shader_compiler/src/vkcv/shader/Compiler.cpp b/modules/shader_compiler/src/vkcv/shader/Compiler.cpp
index f5ec0435ca8b82dc5f328921f43a39338d1be456..1467bf22616764e00399ada69d5f866b929416d5 100644
--- a/modules/shader_compiler/src/vkcv/shader/Compiler.cpp
+++ b/modules/shader_compiler/src/vkcv/shader/Compiler.cpp
@@ -3,6 +3,27 @@
 
 namespace vkcv::shader {
 	
+	void Compiler::compileProgram(ShaderProgram& program,
+								  const std::unordered_map<ShaderStage, const std::filesystem::path>& stages,
+								  const ShaderProgramCompiledFunction& compiled,
+								  const std::filesystem::path& includePath, bool update) {
+		for (const auto& stage : stages) {
+			compile(
+				stage.first,
+				stage.second,
+				[&program](ShaderStage shaderStage, const std::filesystem::path& path) {
+					program.addShader(shaderStage, path);
+				},
+				includePath,
+				update
+			);
+		}
+		
+		if (compiled) {
+			compiled(program);
+		}
+	}
+	
 	std::string Compiler::getDefine(const std::string &name) const {
 		return m_defines.at(name);
 	}
diff --git a/modules/upscaling/config/FidelityFX_FSR.cmake b/modules/upscaling/config/FidelityFX_FSR.cmake
index cc52b4189f781f534a933feb7b782b6bec333e5a..780f1d441abe2a567db7491ce5e2f4821a396b89 100644
--- a/modules/upscaling/config/FidelityFX_FSR.cmake
+++ b/modules/upscaling/config/FidelityFX_FSR.cmake
@@ -1,5 +1,7 @@
 
-if (EXISTS "${vkcv_upscaling_lib_path}/FidelityFX-FSR")
+use_git_submodule("${vkcv_upscaling_lib_path}/FidelityFX-FSR" ffx_fsr_status)
+
+if (${ffx_fsr_status})
 	include_shader(${vkcv_upscaling_lib_path}/FidelityFX-FSR/ffx-fsr/ffx_a.h ${vkcv_upscaling_include} ${vkcv_upscaling_source})
 	include_shader(${vkcv_upscaling_lib_path}/FidelityFX-FSR/ffx-fsr/ffx_fsr1.h ${vkcv_upscaling_include} ${vkcv_upscaling_source})
 	include_shader(${vkcv_upscaling_lib_path}/FidelityFX-FSR/sample/src/VK/FSR_Pass.glsl ${vkcv_upscaling_include} ${vkcv_upscaling_source})
@@ -13,6 +15,4 @@ if (EXISTS "${vkcv_upscaling_lib_path}/FidelityFX-FSR")
 	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_include}/ffx_a.h.hxx)
 	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_include}/ffx_fsr1.h.hxx)
 	list(APPEND vkcv_upscaling_sources ${vkcv_upscaling_include}/FSR_Pass.glsl.hxx)
-else()
-	message(WARNING "FidelityFX-FSR is required..! Update the submodules!")
 endif ()
diff --git a/modules/upscaling/lib/FidelityFX-FSR b/modules/upscaling/lib/FidelityFX-FSR
index bcffc8171efb80e265991301a49670ed755088dd..a21ffb8f6c13233ba336352bdff293894c706575 160000
--- a/modules/upscaling/lib/FidelityFX-FSR
+++ b/modules/upscaling/lib/FidelityFX-FSR
@@ -1 +1 @@
-Subproject commit bcffc8171efb80e265991301a49670ed755088dd
+Subproject commit a21ffb8f6c13233ba336352bdff293894c706575
diff --git a/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp b/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
index eef49ef8d1617239a362f2790e69e8f358883bc1..752d81e65eeb1fc901abf498b8330b7ae6102c80 100644
--- a/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
+++ b/modules/upscaling/src/vkcv/upscaling/FSRUpscaling.cpp
@@ -232,9 +232,9 @@ namespace vkcv::upscaling {
 				program.addShader(shaderStage, path);
 			});
 
-			m_easuPipeline = m_core.createComputePipeline({ program, 
-				{m_core.getDescriptorSetLayout(m_easuDescriptorSetLayout).vulkanHandle} 
-			});
+			m_easuPipeline = m_core.createComputePipeline({program,{
+				m_easuDescriptorSetLayout
+			}});
 
 			
 			DescriptorWrites writes;
@@ -255,7 +255,7 @@ namespace vkcv::upscaling {
 			});
 
 			m_rcasPipeline = m_core.createComputePipeline({ program, {
-				m_core.getDescriptorSetLayout(m_rcasDescriptorSetLayout).vulkanHandle
+				m_rcasDescriptorSetLayout
 			}});
 
 			DescriptorWrites writes;
@@ -344,9 +344,7 @@ namespace vkcv::upscaling {
 					cmdStream,
 					m_easuPipeline,
 					dispatch,
-					{DescriptorSetUsage(0, m_core.getDescriptorSet(
-							m_easuDescriptorSet
-					).vulkanHandle, { 0 })},
+					{DescriptorSetUsage(0, m_easuDescriptorSet, { 0 })},
 					PushConstants(0)
 			);
 			
@@ -366,9 +364,7 @@ namespace vkcv::upscaling {
 					cmdStream,
 					m_rcasPipeline,
 					dispatch,
-					{DescriptorSetUsage(0, m_core.getDescriptorSet(
-							m_rcasDescriptorSet
-					).vulkanHandle, { 0 })},
+					{DescriptorSetUsage(0,m_rcasDescriptorSet, { 0 })},
 					PushConstants(0)
 			);
 			
@@ -386,9 +382,7 @@ namespace vkcv::upscaling {
 					cmdStream,
 					m_easuPipeline,
 					dispatch,
-					{DescriptorSetUsage(0, m_core.getDescriptorSet(
-							m_easuDescriptorSet
-					).vulkanHandle, { 0 })},
+					{DescriptorSetUsage(0, m_easuDescriptorSet, { 0 })},
 					PushConstants(0)
 			);
 		}
diff --git a/projects/CMakeLists.txt b/projects/CMakeLists.txt
index 9cf1ff1c9327114d28871f11694a2fb88bc00735..ffe8546c961697869354993539697b118fa8d6be 100644
--- a/projects/CMakeLists.txt
+++ b/projects/CMakeLists.txt
@@ -1,4 +1,6 @@
 
+include(${vkcv_config_ext}/ProjectFix.cmake)
+
 # Add new projects/examples here:
 add_subdirectory(first_triangle)
 add_subdirectory(first_mesh)
@@ -12,4 +14,5 @@ add_subdirectory(indirect_draw)
 add_subdirectory(bindless_textures)
 add_subdirectory(saf_r)
 add_subdirectory(indirect_dispatch)
-add_subdirectory(path_tracer)
\ No newline at end of file
+add_subdirectory(path_tracer)
+add_subdirectory(wobble_bobble)
\ No newline at end of file
diff --git a/projects/bindless_textures/src/main.cpp b/projects/bindless_textures/src/main.cpp
index 555e3dc65016b0e383fc57ebc3618a35485e9519..6aa0a9d6106dd06c08dd5219c9570430b49ce229 100644
--- a/projects/bindless_textures/src/main.cpp
+++ b/projects/bindless_textures/src/main.cpp
@@ -123,18 +123,12 @@ int main(int argc, const char** argv) {
 	vkcv::ShaderProgram firstMeshProgram;
 	vkcv::shader::GLSLCompiler compiler;
 	
-	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("resources/shaders/shader.vert"),
-					 [&firstMeshProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		firstMeshProgram.addShader(shaderStage, path);
-	});
-	
-	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("resources/shaders/shader.frag"),
-					 [&firstMeshProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		firstMeshProgram.addShader(shaderStage, path);
-	});
+	compiler.compileProgram(firstMeshProgram, {
+		{ vkcv::ShaderStage::VERTEX, "resources/shaders/shader.vert" },
+		{ vkcv::ShaderStage::FRAGMENT, "resources/shaders/shader.frag" }
+	}, nullptr);
  
 	auto& attributes = mesh.vertexGroups[0].vertexBuffer.attributes;
-
 	
 	std::sort(attributes.begin(), attributes.end(), [](const vkcv::asset::VertexAttribute& x, const vkcv::asset::VertexAttribute& y) {
 		return static_cast<uint32_t>(x.type) < static_cast<uint32_t>(y.type);
@@ -162,7 +156,7 @@ int main(int argc, const char** argv) {
         UINT32_MAX,
         firstMeshPass,
         {firstMeshLayout},
-		{ core.getDescriptorSetLayout(descriptorSetLayout).vulkanHandle },
+		{ descriptorSetLayout },
 		true
 	};
 	vkcv::GraphicsPipelineHandle firstMeshPipeline = core.createGraphicsPipeline(firstMeshPipelineConfig);
@@ -218,7 +212,7 @@ int main(int argc, const char** argv) {
 
 	const vkcv::Mesh renderMesh(vertexBufferBindings, indexBuffer.getVulkanHandle(), mesh.vertexGroups[0].numIndices);
 
-	vkcv::DescriptorSetUsage    descriptorUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle);
+	vkcv::DescriptorSetUsage    descriptorUsage(0, descriptorSet);
 	vkcv::DrawcallInfo          drawcall(renderMesh, { descriptorUsage },1);
 
     vkcv::camera::CameraManager cameraManager(core.getWindow(windowHandle));
diff --git a/projects/first_mesh/CMakeLists.txt b/projects/first_mesh/CMakeLists.txt
index 345324ac1d2c836b174c90d16c710491ee477f93..946031f922d7f6b4e2fc5db43c2ef92d48de2691 100644
--- a/projects/first_mesh/CMakeLists.txt
+++ b/projects/first_mesh/CMakeLists.txt
@@ -10,16 +10,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
 
 # adding source files to the project
 add_executable(first_mesh src/main.cpp)
-
-# this should fix the execution path to load local files from the project (for MSVC)
-if(MSVC)
-	set_target_properties(first_mesh PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-	set_target_properties(first_mesh PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-    
-    # in addition to setting the output directory, the working directory has to be set
-	# by default visual studio sets the working directory to the build directory, when using the debugger
-	set_target_properties(first_mesh PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-endif()
+fix_project(first_mesh)
 
 # including headers of dependencies and the VkCV framework
 target_include_directories(first_mesh SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include})
diff --git a/projects/first_mesh/src/main.cpp b/projects/first_mesh/src/main.cpp
index 0871631827b87539bbe9b0050420088e199a39af..3f4378a6a2187ba33b7965fd6d008577541f7351 100644
--- a/projects/first_mesh/src/main.cpp
+++ b/projects/first_mesh/src/main.cpp
@@ -73,18 +73,12 @@ int main(int argc, const char** argv) {
 	vkcv::ShaderProgram firstMeshProgram;
 	vkcv::shader::GLSLCompiler compiler;
 	
-	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("assets/shaders/shader.vert"),
-					 [&firstMeshProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		firstMeshProgram.addShader(shaderStage, path);
-	});
-	
-	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("assets/shaders/shader.frag"),
-					 [&firstMeshProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		firstMeshProgram.addShader(shaderStage, path);
-	});
+	compiler.compileProgram(firstMeshProgram, {
+		{ vkcv::ShaderStage::VERTEX, "assets/shaders/shader.vert" },
+		{ vkcv::ShaderStage::FRAGMENT, "assets/shaders/shader.frag" }
+	}, nullptr);
  
 	auto& attributes = mesh.vertexGroups[0].vertexBuffer.attributes;
-
 	
 	std::sort(attributes.begin(), attributes.end(), [](const vkcv::asset::VertexAttribute& x, const vkcv::asset::VertexAttribute& y) {
 		return static_cast<uint32_t>(x.type) < static_cast<uint32_t>(y.type);
@@ -115,7 +109,7 @@ int main(int argc, const char** argv) {
         UINT32_MAX,
         firstMeshPass,
         {firstMeshLayout},
-		{ core.getDescriptorSetLayout(setLayoutHandle).vulkanHandle },
+		{ setLayoutHandle },
 		true
 	};
 	vkcv::GraphicsPipelineHandle firstMeshPipeline = core.createGraphicsPipeline(firstMeshPipelineConfig);
@@ -167,7 +161,7 @@ int main(int argc, const char** argv) {
 
 	const vkcv::Mesh renderMesh(vertexBufferBindings, indexBuffer.getVulkanHandle(), mesh.vertexGroups[0].numIndices);
 
-	vkcv::DescriptorSetUsage    descriptorUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle);
+	vkcv::DescriptorSetUsage    descriptorUsage(0, descriptorSet);
 	vkcv::DrawcallInfo          drawcall(renderMesh, { descriptorUsage },1);
 
     vkcv::camera::CameraManager cameraManager(core.getWindow(windowHandle));
diff --git a/projects/first_scene/CMakeLists.txt b/projects/first_scene/CMakeLists.txt
index d55476f5fa62639aeaec7a3859f2f63f9c937aed..3d0a5ee7e32ea1ad4cfdd0744e00ad15d69a7541 100644
--- a/projects/first_scene/CMakeLists.txt
+++ b/projects/first_scene/CMakeLists.txt
@@ -10,16 +10,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
 
 # adding source files to the project
 add_executable(first_scene src/main.cpp)
-
-# this should fix the execution path to load local files from the project (for MSVC)
-if(MSVC)
-	set_target_properties(first_scene PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-	set_target_properties(first_scene PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-    
-    # in addition to setting the output directory, the working directory has to be set
-	# by default visual studio sets the working directory to the build directory, when using the debugger
-	set_target_properties(first_scene PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-endif()
+fix_project(first_scene)
 
 # including headers of dependencies and the VkCV framework
 target_include_directories(first_scene SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include} ${vkcv_scene_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include})
diff --git a/projects/first_scene/src/main.cpp b/projects/first_scene/src/main.cpp
index a078776eb4b7688f51d45cecb7e08d5ba9d0d973..3dd6fc40d516c6f1ae3358d60c02fc4ed219245a 100644
--- a/projects/first_scene/src/main.cpp
+++ b/projects/first_scene/src/main.cpp
@@ -61,15 +61,10 @@ int main(int argc, const char** argv) {
 	vkcv::ShaderProgram sceneShaderProgram;
 	vkcv::shader::GLSLCompiler compiler;
 	
-	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("assets/shaders/shader.vert"),
-					 [&sceneShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		sceneShaderProgram.addShader(shaderStage, path);
-	});
-	
-	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("assets/shaders/shader.frag"),
-					 [&sceneShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		sceneShaderProgram.addShader(shaderStage, path);
-	});
+	compiler.compileProgram(sceneShaderProgram, {
+		{ vkcv::ShaderStage::VERTEX, "assets/shaders/shader.vert" },
+		{ vkcv::ShaderStage::FRAGMENT, "assets/shaders/shader.frag" }
+	}, nullptr);
 
 	const std::vector<vkcv::VertexAttachment> vertexAttachments = sceneShaderProgram.getVertexAttachments();
 	std::vector<vkcv::VertexBinding> bindings;
@@ -87,8 +82,9 @@ int main(int argc, const char** argv) {
 		UINT32_MAX,
 		scenePass,
 		{sceneLayout},
-		{ core.getDescriptorSetLayout(material0.getDescriptorSetLayout()).vulkanHandle },
-		true };
+		{ material0.getDescriptorSetLayout() },
+		true
+	};
 	vkcv::GraphicsPipelineHandle scenePipeline = core.createGraphicsPipeline(scenePipelineDefinition);
 	
 	if (!scenePipeline) {
diff --git a/projects/first_triangle/CMakeLists.txt b/projects/first_triangle/CMakeLists.txt
index cf1fdb9022fc8332210a44946911fbfab3f5559a..793c2bc37db08c9fe744a56c97f474e1e2d17c4f 100644
--- a/projects/first_triangle/CMakeLists.txt
+++ b/projects/first_triangle/CMakeLists.txt
@@ -10,16 +10,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
 
 # adding source files to the project
 add_executable(first_triangle src/main.cpp)
-
-# this should fix the execution path to load local files from the project (for MSVC)
-if(MSVC)
-	set_target_properties(first_triangle PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-	set_target_properties(first_triangle PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-
-	# in addition to setting the output directory, the working directory has to be set
-	# by default visual studio sets the working directory to the build directory, when using the debugger
-	set_target_properties(first_triangle PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-endif()
+fix_project(first_triangle)
 
 # including headers of dependencies and the VkCV framework
 target_include_directories(first_triangle SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include})
diff --git a/projects/first_triangle/src/main.cpp b/projects/first_triangle/src/main.cpp
index 1725b5c84dea931fe785880e3cd423cbb5f04a46..b4ba9046e07ecd3534844b493642098aa5847307 100644
--- a/projects/first_triangle/src/main.cpp
+++ b/projects/first_triangle/src/main.cpp
@@ -47,15 +47,10 @@ int main(int argc, const char** argv) {
 	vkcv::ShaderProgram triangleShaderProgram;
 	vkcv::shader::GLSLCompiler compiler;
 	
-	compiler.compile(vkcv::ShaderStage::VERTEX, std::filesystem::path("shaders/shader.vert"),
-					 [&triangleShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		 triangleShaderProgram.addShader(shaderStage, path);
-	});
-	
-	compiler.compile(vkcv::ShaderStage::FRAGMENT, std::filesystem::path("shaders/shader.frag"),
-					 [&triangleShaderProgram](vkcv::ShaderStage shaderStage, const std::filesystem::path& path) {
-		triangleShaderProgram.addShader(shaderStage, path);
-	});
+	compiler.compileProgram(triangleShaderProgram, {
+		{vkcv::ShaderStage::VERTEX, "shaders/shader.vert"},
+		{ vkcv::ShaderStage::FRAGMENT, "shaders/shader.frag" }
+	}, nullptr);
 	
 	const auto swapchainExtent = core.getSwapchain(windowHandle).getExtent();
 
diff --git a/projects/indirect_dispatch/CMakeLists.txt b/projects/indirect_dispatch/CMakeLists.txt
index ac8501f17b65212f87d00998dbf9bc4d5c044619..18a89c8e1966b1e0ee509afd716c1410d9d671d8 100644
--- a/projects/indirect_dispatch/CMakeLists.txt
+++ b/projects/indirect_dispatch/CMakeLists.txt
@@ -26,16 +26,8 @@ target_sources(indirect_dispatch PRIVATE
     
     src/MotionBlurSetup.hpp
     src/MotionBlurSetup.cpp)
-    
-# this should fix the execution path to load local files from the project (for MSVC)
-if(MSVC)
-	set_target_properties(indirect_dispatch PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-	set_target_properties(indirect_dispatch PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-
-	# in addition to setting the output directory, the working directory has to be set
-	# by default visual studio sets the working directory to the build directory, when using the debugger
-	set_target_properties(indirect_dispatch PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-endif()
+
+fix_project(indirect_dispatch)
 
 # including headers of dependencies and the VkCV framework
 target_include_directories(indirect_dispatch SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include})
diff --git a/projects/indirect_dispatch/src/App.cpp b/projects/indirect_dispatch/src/App.cpp
index 532cef11db5b2bb8b5d741f6505507ff23fa4163..cc897e68651de65c7122188b52598535e6637335 100644
--- a/projects/indirect_dispatch/src/App.cpp
+++ b/projects/indirect_dispatch/src/App.cpp
@@ -261,7 +261,7 @@ void App::run() {
 		for (const Object& obj : sceneObjects) {
 			forwardSceneDrawcalls.push_back(vkcv::DrawcallInfo(
 				obj.meshResources.mesh, 
-				{ vkcv::DescriptorSetUsage(0, m_core.getDescriptorSet(m_meshPass.descriptorSet).vulkanHandle) }));
+				{ vkcv::DescriptorSetUsage(0, m_meshPass.descriptorSet) }));
 		}
 
 		m_core.recordDrawcallsToCmdStream(
@@ -338,7 +338,7 @@ void App::run() {
 			cmdStream,
 			m_gammaCorrectionPass.pipeline,
 			fullScreenImageDispatch,
-			{ vkcv::DescriptorSetUsage(0, m_core.getDescriptorSet(m_gammaCorrectionPass.descriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, m_gammaCorrectionPass.descriptorSet) },
 			vkcv::PushConstants(0));
 
 		m_core.prepareSwapchainImageForPresent(cmdStream);
diff --git a/projects/indirect_dispatch/src/AppSetup.cpp b/projects/indirect_dispatch/src/AppSetup.cpp
index e1b29ad5990d371535794fa72a1d80e2b653045b..1d40204b1ab7c6cd49a0c7924fa1fdbf6c2cc24e 100644
--- a/projects/indirect_dispatch/src/AppSetup.cpp
+++ b/projects/indirect_dispatch/src/AppSetup.cpp
@@ -122,12 +122,12 @@ bool loadGraphicPass(
 
 	const auto descriptorBindings = shaderProgram.getReflectedDescriptors();
 	const bool hasDescriptor = descriptorBindings.size() > 0;
-	std::vector<vk::DescriptorSetLayout> descriptorSetLayouts = {};
+	std::vector<vkcv::DescriptorSetLayoutHandle> descriptorSetLayouts = {};
 	if (hasDescriptor)
 	{
 	    outPassHandles->descriptorSetLayout = core.createDescriptorSetLayout(descriptorBindings.at(0));
 	    outPassHandles->descriptorSet = core.createDescriptorSet(outPassHandles->descriptorSetLayout);
-	    descriptorSetLayouts.push_back(core.getDescriptorSetLayout(outPassHandles->descriptorSetLayout).vulkanHandle);
+	    descriptorSetLayouts.push_back(outPassHandles->descriptorSetLayout);
 	}
 
 
@@ -138,7 +138,8 @@ bool loadGraphicPass(
 		outPassHandles->renderPass,
 		{ vertexLayout },
 		descriptorSetLayouts,
-		true };
+		true
+	};
 	pipelineConfig.m_depthTest  = depthTest;
 	outPassHandles->pipeline    = core.createGraphicsPipeline(pipelineConfig);
 
@@ -260,7 +261,7 @@ bool loadComputePass(vkcv::Core& core, const std::filesystem::path& path, Comput
 	outComputePass->descriptorSet = core.createDescriptorSet(outComputePass->descriptorSetLayout);
 	outComputePass->pipeline = core.createComputePipeline({
 		shaderProgram,
-		{ core.getDescriptorSetLayout(outComputePass->descriptorSetLayout).vulkanHandle }});
+		{ outComputePass->descriptorSetLayout }});
 
 	if (!outComputePass->pipeline) {
 		vkcv_log(vkcv::LogLevel::ERROR, "Compute shader pipeline creation failed");
diff --git a/projects/indirect_dispatch/src/MotionBlur.cpp b/projects/indirect_dispatch/src/MotionBlur.cpp
index fea5b1d6f726851ad1d88e71301bffb0f6d71f4a..30116c84b79ae71c683ee865dbf93dd1a37923e9 100644
--- a/projects/indirect_dispatch/src/MotionBlur.cpp
+++ b/projects/indirect_dispatch/src/MotionBlur.cpp
@@ -120,7 +120,7 @@ vkcv::ImageHandle MotionBlur::render(
 		cmdStream,
 		m_tileResetPass.pipeline,
 		dispatchSizeOne,
-		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_tileResetPass.descriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_tileResetPass.descriptorSet) },
 		vkcv::PushConstants(0));
 
 	m_core->recordBufferMemoryBarrier(cmdStream, m_fullPathWorkTileBuffer);
@@ -166,7 +166,7 @@ vkcv::ImageHandle MotionBlur::render(
 		cmdStream,
 		m_tileClassificationPass.pipeline,
 		tileClassificationDispatch.data(),
-		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_tileClassificationPass.descriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_tileClassificationPass.descriptorSet) },
 		classificationPushConstants);
 
 	m_core->recordBufferMemoryBarrier(cmdStream, m_fullPathWorkTileBuffer);
@@ -254,7 +254,7 @@ vkcv::ImageHandle MotionBlur::render(
 			m_motionBlurPass.pipeline,
 			m_fullPathWorkTileBuffer,
 			0,
-			{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionBlurPass.descriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, m_motionBlurPass.descriptorSet) },
 			motionBlurPushConstants);
 
 		m_core->recordComputeIndirectDispatchToCmdStream(
@@ -262,7 +262,7 @@ vkcv::ImageHandle MotionBlur::render(
 			m_colorCopyPass.pipeline,
 			m_copyPathWorkTileBuffer,
 			0,
-			{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_colorCopyPass.descriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, m_colorCopyPass.descriptorSet) },
 			vkcv::PushConstants(0));
 
 		m_core->recordComputeIndirectDispatchToCmdStream(
@@ -270,7 +270,7 @@ vkcv::ImageHandle MotionBlur::render(
 			m_motionBlurFastPathPass.pipeline,
 			m_fastPathWorkTileBuffer,
 			0,
-			{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionBlurFastPathPass.descriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, m_motionBlurFastPathPass.descriptorSet) },
 			fastPathPushConstants);
 	}
 	else if(mode == eMotionBlurMode::Disabled) {
@@ -305,7 +305,7 @@ vkcv::ImageHandle MotionBlur::render(
 			cmdStream,
 			m_tileVisualisationPass.pipeline,
 			dispatchCounts,
-			{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_tileVisualisationPass.descriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, m_tileVisualisationPass.descriptorSet) },
 			vkcv::PushConstants(0));
 	}
 	else {
@@ -371,7 +371,7 @@ vkcv::ImageHandle MotionBlur::renderMotionVectorVisualisation(
 		cmdStream,
 		m_motionVectorVisualisationPass.pipeline,
 		dispatchSizes.data(),
-		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionVectorVisualisationPass.descriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_motionVectorVisualisationPass.descriptorSet) },
 		motionVectorVisualisationPushConstants);
 
 	return m_renderTargets.outputColor;
@@ -406,7 +406,7 @@ void MotionBlur::computeMotionTiles(
 		cmdStream,
 		m_motionVectorMinMaxPass.pipeline,
 		motionTileDispatchCounts.data(),
-		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionVectorMinMaxPass.descriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_motionVectorMinMaxPass.descriptorSet) },
 		vkcv::PushConstants(0));
 
 	// motion vector min max neighbourhood
@@ -432,6 +432,6 @@ void MotionBlur::computeMotionTiles(
 		cmdStream,
 		m_motionVectorMinMaxNeighbourhoodPass.pipeline,
 		motionTileDispatchCounts.data(),
-		{ vkcv::DescriptorSetUsage(0, m_core->getDescriptorSet(m_motionVectorMinMaxNeighbourhoodPass.descriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_motionVectorMinMaxNeighbourhoodPass.descriptorSet) },
 		vkcv::PushConstants(0));
 }
\ No newline at end of file
diff --git a/projects/indirect_draw/src/main.cpp b/projects/indirect_draw/src/main.cpp
index 88e7005f2912bb448980bd4c1219245481f1559a..9b062ab56245c44e5021f164b9e06fed8edd15c3 100644
--- a/projects/indirect_draw/src/main.cpp
+++ b/projects/indirect_draw/src/main.cpp
@@ -467,7 +467,7 @@ int main(int argc, const char** argv) {
         UINT32_MAX,
         passHandle,
         {sponzaVertexLayout},
-		{ core.getDescriptorSetLayout(descriptorSetLayout).vulkanHandle },
+		{ descriptorSetLayout },
 		true
 	};
 	vkcv::GraphicsPipelineHandle sponzaPipelineHandle = core.createGraphicsPipeline(sponzaPipelineConfig);
@@ -512,7 +512,7 @@ int main(int argc, const char** argv) {
 
     const vkcv::ComputePipelineConfig computeCullingConfig {
         cullingProgram,
-        {core.getDescriptorSetLayout(cullingSetLayout).vulkanHandle}
+        {cullingSetLayout}
     };
     vkcv::ComputePipelineHandle cullingPipelineHandle = core.createComputePipeline(computeCullingConfig);
     if (!cullingPipelineHandle) {
@@ -536,7 +536,7 @@ int main(int argc, const char** argv) {
     const uint32_t dispatchCount[3] = {static_cast<uint32_t>(ceiledDispatchCount), 1, 1};
 
 
-    vkcv::DescriptorSetUsage cullingUsage(0, core.getDescriptorSet(cullingDescSet).vulkanHandle, {});
+    vkcv::DescriptorSetUsage cullingUsage(0, cullingDescSet, {});
     vkcv::PushConstants emptyPushConstant(0);
 
     bool updateFrustumPlanes    = true;
diff --git a/projects/mesh_shader/CMakeLists.txt b/projects/mesh_shader/CMakeLists.txt
index 9e132e555136a7dfdd7d8fc15e06547d8059aff5..0eb0ca2e45daa7486a80ea897a2397c1d3ac1ba3 100644
--- a/projects/mesh_shader/CMakeLists.txt
+++ b/projects/mesh_shader/CMakeLists.txt
@@ -10,18 +10,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
 
 # adding source files to the project
 add_executable(mesh_shader src/main.cpp)
-
-target_sources(mesh_shader PRIVATE)
-
-# this should fix the execution path to load local files from the project (for MSVC)
-if(MSVC)
-	set_target_properties(mesh_shader PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-	set_target_properties(mesh_shader PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-
-	# in addition to setting the output directory, the working directory has to be set
-	# by default visual studio sets the working directory to the build directory, when using the debugger
-	set_target_properties(mesh_shader PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-endif()
+fix_project(mesh_shader)
 
 # including headers of dependencies and the VkCV framework
 target_include_directories(mesh_shader SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_camera_include} ${vkcv_meshlet_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include})
diff --git a/projects/mesh_shader/src/main.cpp b/projects/mesh_shader/src/main.cpp
index 0ee16b99822e0c8fbeaf58af57f02255493b0fb5..0a9914abf0a28f82eee06c5d2a67067faaff4109 100644
--- a/projects/mesh_shader/src/main.cpp
+++ b/projects/mesh_shader/src/main.cpp
@@ -211,7 +211,7 @@ int main(int argc, const char** argv) {
 			swapchainExtent.height,
 			renderPass,
 			{ bunnyLayout },
-			{ core.getDescriptorSetLayout(vertexShaderDescriptorSetLayout).vulkanHandle },
+			{ vertexShaderDescriptorSetLayout },
 			false
 	};
 
@@ -261,7 +261,7 @@ int main(int argc, const char** argv) {
 		swapchainExtent.height,
 		renderPass,
 		{meshShaderLayout},
-		{core.getDescriptorSetLayout(meshShaderDescriptorSetLayout).vulkanHandle},
+		{meshShaderDescriptorSetLayout},
 		false
 	};
 
@@ -354,7 +354,7 @@ int main(int argc, const char** argv) {
 
 		if (useMeshShader) {
 
-			vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(meshShaderDescriptorSet).vulkanHandle);
+			vkcv::DescriptorSetUsage descriptorUsage(0, meshShaderDescriptorSet);
 			const uint32_t taskCount = (meshShaderModelData.meshlets.size() + 31) / 32;
 
 			core.recordMeshShaderDrawcalls(
@@ -368,7 +368,7 @@ int main(int argc, const char** argv) {
 		}
 		else {
 
-			vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(vertexShaderDescriptorSet).vulkanHandle);
+			vkcv::DescriptorSetUsage descriptorUsage(0, vertexShaderDescriptorSet);
 
 			core.recordDrawcallsToCmdStream(
 				cmdStream,
diff --git a/projects/particle_simulation/CMakeLists.txt b/projects/particle_simulation/CMakeLists.txt
index 41ce6ca2f4042285ad42d81e79f04d7b53c837e7..2ff6aaf62c6a9842c3d1b9669286becfd79d92ca 100644
--- a/projects/particle_simulation/CMakeLists.txt
+++ b/projects/particle_simulation/CMakeLists.txt
@@ -18,15 +18,7 @@ add_executable(particle_simulation
 		src/BloomAndFlares.hpp
 		src/BloomAndFlares.cpp)
 
-# this should fix the execution path to load local files from the project (for MSVC)
-if(MSVC)
-	set_target_properties(particle_simulation PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-	set_target_properties(particle_simulation PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-
-	# in addition to setting the output directory, the working directory has to be set
-	# by default visual studio sets the working directory to the build directory, when using the debugger
-	set_target_properties(particle_simulation PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-endif()
+fix_project(particle_simulation)
 
 # including headers of dependencies and the VkCV framework
 target_include_directories(particle_simulation SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_testing_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include})
diff --git a/projects/particle_simulation/src/BloomAndFlares.cpp b/projects/particle_simulation/src/BloomAndFlares.cpp
index d0f25122c443eef847ce190c099eef95e8ecc0c8..6ab0a8deff3d5fe906567562cb86d75a1cc2c09b 100644
--- a/projects/particle_simulation/src/BloomAndFlares.cpp
+++ b/projects/particle_simulation/src/BloomAndFlares.cpp
@@ -38,7 +38,8 @@ BloomAndFlares::BloomAndFlares(
     }
 
     m_DownsamplePipe = p_Core->createComputePipeline({
-        dsProg, { p_Core->getDescriptorSetLayout(m_DownsampleDescSetLayouts[0]).vulkanHandle }});
+        dsProg, m_DownsampleDescSetLayouts
+	});
 
     // UPSAMPLE
     vkcv::ShaderProgram usProg;
@@ -57,7 +58,8 @@ BloomAndFlares::BloomAndFlares(
     }
 
     m_UpsamplePipe = p_Core->createComputePipeline({
-            usProg, { p_Core->getDescriptorSetLayout(m_UpsampleDescSetLayouts[0]).vulkanHandle }});
+            usProg, m_UpsampleDescSetLayouts
+	});
 
     // LENS FEATURES
     vkcv::ShaderProgram lensProg;
@@ -71,7 +73,8 @@ BloomAndFlares::BloomAndFlares(
     m_LensFlareDescSetLayout = p_Core->createDescriptorSetLayout(lensProg.getReflectedDescriptors().at(0));
     m_LensFlareDescSet = p_Core->createDescriptorSet(m_LensFlareDescSetLayout);
     m_LensFlarePipe = p_Core->createComputePipeline({
-            lensProg, { p_Core->getDescriptorSetLayout(m_LensFlareDescSetLayout).vulkanHandle }});
+            lensProg, { m_LensFlareDescSetLayout }
+	});
 
     // COMPOSITE
     vkcv::ShaderProgram compProg;
@@ -85,7 +88,8 @@ BloomAndFlares::BloomAndFlares(
     m_CompositeDescSetLayout = p_Core->createDescriptorSetLayout(compProg.getReflectedDescriptors().at(0));
     m_CompositeDescSet = p_Core->createDescriptorSet(m_CompositeDescSetLayout);
     m_CompositePipe = p_Core->createComputePipeline({
-            compProg, { p_Core->getDescriptorSetLayout(m_CompositeDescSetLayout).vulkanHandle }});
+            compProg, { m_CompositeDescSetLayout }
+	});
 }
 
 void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream,
@@ -114,7 +118,7 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
             cmdStream,
             m_DownsamplePipe,
             initialDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[0]).vulkanHandle)},
+            {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[0])},
             vkcv::PushConstants(0));
 
     // downsample dispatches of blur buffer's mip maps
@@ -149,7 +153,7 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
                 cmdStream,
                 m_DownsamplePipe,
                 mipDispatchCount,
-                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[mipLevel]).vulkanHandle)},
+                {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[mipLevel])},
                 vkcv::PushConstants(0));
 
         // image barrier between mips
@@ -194,7 +198,7 @@ void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream
                 cmdStream,
                 m_UpsamplePipe,
                 upsampleDispatchCount,
-                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_UpsampleDescSets[mipLevel]).vulkanHandle)},
+                {vkcv::DescriptorSetUsage(0, m_UpsampleDescSets[mipLevel])},
                 vkcv::PushConstants(0)
         );
         // image barrier between mips
@@ -226,7 +230,7 @@ void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStr
             cmdStream,
             m_LensFlarePipe,
             lensFeatureDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_LensFlareDescSet).vulkanHandle)},
+            {vkcv::DescriptorSetUsage(0, m_LensFlareDescSet)},
             vkcv::PushConstants(0));
 }
 
@@ -259,7 +263,7 @@ void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStrea
             cmdStream,
             m_CompositePipe,
             compositeDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_CompositeDescSet).vulkanHandle)},
+            {vkcv::DescriptorSetUsage(0, m_CompositeDescSet)},
             vkcv::PushConstants(0));
 }
 
diff --git a/projects/particle_simulation/src/main.cpp b/projects/particle_simulation/src/main.cpp
index ae3c6795a66cdc81297986acb224a63055d02c44..6637041e5ea9c8f1dd3dadf0303049d2a3b749e9 100644
--- a/projects/particle_simulation/src/main.cpp
+++ b/projects/particle_simulation/src/main.cpp
@@ -121,8 +121,9 @@ int main(int argc, const char **argv) {
             UINT32_MAX,
             particlePass,
             {particleLayout},
-            {core.getDescriptorSetLayout(descriptorSetLayout).vulkanHandle},
-            true};
+            {descriptorSetLayout},
+            true
+	};
     particlePipelineDefinition.m_blendMode = vkcv::BlendMode::Additive;
 
     const std::vector<glm::vec3> vertices = {glm::vec3(-0.012, 0.012, 0),
@@ -133,7 +134,9 @@ int main(int argc, const char **argv) {
 
     vkcv::GraphicsPipelineHandle particlePipeline = core.createGraphicsPipeline(particlePipelineDefinition);
 
-    vkcv::ComputePipelineHandle computePipeline = core.createComputePipeline({ computeShaderProgram, {core.getDescriptorSetLayout(computeDescriptorSetLayout).vulkanHandle} });
+    vkcv::ComputePipelineHandle computePipeline = core.createComputePipeline({
+		computeShaderProgram, {computeDescriptorSetLayout}
+	});
 
     vkcv::Buffer<glm::vec4> color = core.createBuffer<glm::vec4>(
             vkcv::BufferType::UNIFORM,
@@ -177,7 +180,7 @@ int main(int argc, const char **argv) {
 
     const vkcv::Mesh renderMesh({vertexBufferBindings}, particleIndexBuffer.getVulkanHandle(),
                                 particleIndexBuffer.getCount());
-    vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle);
+    vkcv::DescriptorSetUsage descriptorUsage(0, descriptorSet);
 
     auto pos = glm::vec2(0.f);
     auto spawnPosition = glm::vec3(0.f);
@@ -238,7 +241,8 @@ int main(int argc, const char **argv) {
     vkcv::DescriptorSetHandle tonemappingDescriptor = core.createDescriptorSet(tonemappingDescriptorLayout);
     vkcv::ComputePipelineHandle tonemappingPipe = core.createComputePipeline({
         tonemappingShader, 
-        { core.getDescriptorSetLayout(tonemappingDescriptorLayout).vulkanHandle }});
+        { tonemappingDescriptorLayout }
+	});
 
     std::uniform_real_distribution<float> rdm = std::uniform_real_distribution<float>(0.95f, 1.05f);
     std::default_random_engine rdmEngine;
@@ -279,7 +283,7 @@ int main(int argc, const char **argv) {
         core.recordComputeDispatchToCmdStream(cmdStream,
                                               computePipeline,
                                               computeDispatchCount,
-                                              {vkcv::DescriptorSetUsage(0,core.getDescriptorSet(computeDescriptorSet).vulkanHandle)},
+                                              {vkcv::DescriptorSetUsage(0, computeDescriptorSet)},
 											  pushConstantsCompute);
 
         core.recordBufferMemoryBarrier(cmdStream, particleBuffer.getHandle());
@@ -317,7 +321,7 @@ int main(int argc, const char **argv) {
             cmdStream, 
             tonemappingPipe, 
             tonemappingDispatchCount, 
-            {vkcv::DescriptorSetUsage(0, core.getDescriptorSet(tonemappingDescriptor).vulkanHandle) },
+            {vkcv::DescriptorSetUsage(0, tonemappingDescriptor) },
             vkcv::PushConstants(0));
 
         core.prepareSwapchainImageForPresent(cmdStream);
diff --git a/projects/path_tracer/src/main.cpp b/projects/path_tracer/src/main.cpp
index c49bb7ed7f2cd65345ef273d0b17273101a92df0..b76244b035d8d24e1abd665b82d09bd413a14a09 100644
--- a/projects/path_tracer/src/main.cpp
+++ b/projects/path_tracer/src/main.cpp
@@ -104,7 +104,7 @@ int main(int argc, const char** argv) {
 	vkcv::DescriptorSetHandle       imageCombineDescriptorSet       = core.createDescriptorSet(imageCombineDescriptorSetLayout);
 	vkcv::ComputePipelineHandle     imageCombinePipeline            = core.createComputePipeline({
 		imageCombineShaderProgram, 
-		{ core.getDescriptorSetLayout(imageCombineDescriptorSetLayout).vulkanHandle }
+		{ imageCombineDescriptorSetLayout }
 	});
 
 	vkcv::DescriptorWrites imageCombineDescriptorWrites;
@@ -126,7 +126,7 @@ int main(int argc, const char** argv) {
 	vkcv::DescriptorSetHandle       presentDescriptorSet        = core.createDescriptorSet(presentDescriptorSetLayout);
 	vkcv::ComputePipelineHandle     presentPipeline             = core.createComputePipeline({
 		presentShaderProgram,
-		{ core.getDescriptorSetLayout(presentDescriptorSetLayout).vulkanHandle }
+		{ presentDescriptorSetLayout }
 	});
 
 	// clear shader
@@ -141,7 +141,7 @@ int main(int argc, const char** argv) {
 	vkcv::DescriptorSetHandle       imageClearDescriptorSet         = core.createDescriptorSet(imageClearDescriptorSetLayout);
 	vkcv::ComputePipelineHandle     imageClearPipeline              = core.createComputePipeline({
 		clearShaderProgram,
-		{ core.getDescriptorSetLayout(imageClearDescriptorSetLayout).vulkanHandle }
+		{ imageClearDescriptorSetLayout }
 	});
 
 	vkcv::DescriptorWrites imageClearDescriptorWrites;
@@ -204,7 +204,7 @@ int main(int argc, const char** argv) {
 
 	vkcv::ComputePipelineHandle tracePipeline = core.createComputePipeline({
 		traceShaderProgram,
-		{ core.getDescriptorSetLayout(traceDescriptorSetLayout).vulkanHandle }
+		{ traceDescriptorSetLayout }
 	});
 
 	if (!tracePipeline)
@@ -343,7 +343,7 @@ int main(int argc, const char** argv) {
 			core.recordComputeDispatchToCmdStream(cmdStream,
 				imageClearPipeline,
 				fullscreenDispatchCount,
-				{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(imageClearDescriptorSet).vulkanHandle) },
+				{ vkcv::DescriptorSetUsage(0, imageClearDescriptorSet) },
 				vkcv::PushConstants(0));
 
 			clearMeanImage = false;
@@ -378,7 +378,7 @@ int main(int argc, const char** argv) {
 		core.recordComputeDispatchToCmdStream(cmdStream,
 			tracePipeline,
 			traceDispatchCount,
-			{ vkcv::DescriptorSetUsage(0,core.getDescriptorSet(traceDescriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, traceDescriptorSet) },
 			pushConstantsCompute);
 
 		core.prepareImageForStorage(cmdStream, meanImage);
@@ -388,7 +388,7 @@ int main(int argc, const char** argv) {
 		core.recordComputeDispatchToCmdStream(cmdStream,
 			imageCombinePipeline,
 			fullscreenDispatchCount,
-			{ vkcv::DescriptorSetUsage(0,core.getDescriptorSet(imageCombineDescriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, imageCombineDescriptorSet) },
 			vkcv::PushConstants(0));
 
 		core.recordImageMemoryBarrier(cmdStream, meanImage);
@@ -407,7 +407,7 @@ int main(int argc, const char** argv) {
 		core.recordComputeDispatchToCmdStream(cmdStream,
 			presentPipeline,
 			fullscreenDispatchCount,
-			{ vkcv::DescriptorSetUsage(0,core.getDescriptorSet(presentDescriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, presentDescriptorSet) },
 			vkcv::PushConstants(0));
 
 		core.prepareSwapchainImageForPresent(cmdStream);
diff --git a/projects/rtx_ambient_occlusion/src/RTX/ASManager.cpp b/projects/rtx_ambient_occlusion/src/RTX/ASManager.cpp
index 186379a63a5844bf43bdf0e9d1ee7572d8077f52..8e19caaeda878d8b37b24069bbf752bae6dc41d0 100644
--- a/projects/rtx_ambient_occlusion/src/RTX/ASManager.cpp
+++ b/projects/rtx_ambient_occlusion/src/RTX/ASManager.cpp
@@ -323,7 +323,9 @@ namespace vkcv::rtx {
         bufferInstances.bufferType = RTXBufferType::GPU;
         bufferInstances.deviceSize = sizeof(accelerationStructureInstanceKhr);
         bufferInstances.bufferUsageFlagBits = vk::BufferUsageFlagBits::eShaderDeviceAddress
-            | vk::BufferUsageFlagBits::eTransferDst | vk::BufferUsageFlagBits::eTransferSrc;
+            | vk::BufferUsageFlagBits::eTransferDst
+			| vk::BufferUsageFlagBits::eTransferSrc
+			| vk::BufferUsageFlagBits::eAccelerationStructureBuildInputReadOnlyKHR;
         bufferInstances.memoryPropertyFlagBits = vk::MemoryPropertyFlagBits::eDeviceLocal;
 
         createBuffer(bufferInstances);
diff --git a/projects/rtx_ambient_occlusion/src/RTX/RTXExtensions.cpp b/projects/rtx_ambient_occlusion/src/RTX/RTXExtensions.cpp
index 133bd8b888f62a56a35c997d2ea75374fbf203a7..0bc6a4d3dff358bdeab74d9d5da695f81c3675c7 100644
--- a/projects/rtx_ambient_occlusion/src/RTX/RTXExtensions.cpp
+++ b/projects/rtx_ambient_occlusion/src/RTX/RTXExtensions.cpp
@@ -11,12 +11,8 @@ RTXExtensions::RTXExtensions()
     };
     m_deviceExtensions = {
             VK_KHR_MAINTENANCE3_EXTENSION_NAME,
-            VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME,
-            VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME,
             VK_KHR_DEFERRED_HOST_OPERATIONS_EXTENSION_NAME,
-            VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME,
             VK_KHR_SPIRV_1_4_EXTENSION_NAME,
-            VK_KHR_RAY_TRACING_PIPELINE_EXTENSION_NAME,
             VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME
     };
 
@@ -24,91 +20,32 @@ RTXExtensions::RTXExtensions()
     for (auto deviceExtension : m_deviceExtensions) {
         m_features.requireExtension(deviceExtension);
     }
-
-    /* FIXME: We must disable features that will be mentioned as "not supported" by the FeatureManager. If every unsupported feature is disabled, this should work.
-     * Maybe we find a better workaround...
-     */
-    m_features.requireFeature<vk::PhysicalDeviceVulkan12Features>(
-        [](vk::PhysicalDeviceVulkan12Features& features) {
-            features.setSamplerMirrorClampToEdge(true);
-            features.setDrawIndirectCount(true);
-            features.setStorageBuffer8BitAccess(true);
-            features.setUniformAndStorageBuffer8BitAccess(true);
-            features.setStoragePushConstant8(true);
-            features.setShaderBufferInt64Atomics(true);
-            features.setShaderSharedInt64Atomics(true);
-            features.setShaderFloat16(true);
-            features.setShaderInt8(true);
-            features.setDescriptorIndexing(true);
-            features.setShaderInputAttachmentArrayDynamicIndexing(true);
-            features.setShaderUniformTexelBufferArrayDynamicIndexing(true);
-            features.setShaderStorageTexelBufferArrayDynamicIndexing(true);
-            features.setShaderUniformBufferArrayNonUniformIndexing(true);
-            features.setShaderSampledImageArrayNonUniformIndexing(true);
-            features.setShaderStorageBufferArrayNonUniformIndexing(true);
-            features.setShaderStorageImageArrayNonUniformIndexing(true);
-            features.setShaderInputAttachmentArrayNonUniformIndexing(true);
-            features.setShaderUniformTexelBufferArrayNonUniformIndexing(true);
-            features.setShaderStorageTexelBufferArrayNonUniformIndexing(true);
-            features.setDescriptorBindingUniformBufferUpdateAfterBind(true);
-            features.setDescriptorBindingSampledImageUpdateAfterBind(true);
-            features.setDescriptorBindingStorageImageUpdateAfterBind(true);
-            features.setDescriptorBindingStorageBufferUpdateAfterBind(true);
-            features.setDescriptorBindingUniformTexelBufferUpdateAfterBind(true);
-            features.setDescriptorBindingStorageTexelBufferUpdateAfterBind(true);
-            features.setDescriptorBindingUpdateUnusedWhilePending(true);
-            features.setDescriptorBindingPartiallyBound(true);
-            features.setDescriptorBindingVariableDescriptorCount(true);
-            features.setRuntimeDescriptorArray(true);
-            features.setSamplerFilterMinmax(true);
-            features.setScalarBlockLayout(true);
-            features.setImagelessFramebuffer(true);
-            features.setUniformBufferStandardLayout(true);
-            features.setShaderSubgroupExtendedTypes(true);
-            features.setSeparateDepthStencilLayouts(true);
-            features.setHostQueryReset(true);
-            features.setTimelineSemaphore(true);
-            features.setBufferDeviceAddress(true);
-            features.setBufferDeviceAddressCaptureReplay(true);
-            features.setBufferDeviceAddressMultiDevice(true);
-            features.setVulkanMemoryModel(true);
-            features.setVulkanMemoryModelDeviceScope(true);
-            features.setVulkanMemoryModelAvailabilityVisibilityChains(true);
-            features.setShaderOutputViewportIndex(true);
-            features.setShaderOutputLayer(true);
-            features.setSubgroupBroadcastDynamicId(true);
-        });
-    m_features.requireFeature<vk::PhysicalDeviceVulkan11Features>(
-        [](vk::PhysicalDeviceVulkan11Features& features) {
-            features.setMultiview(true);
-            features.setMultiviewGeometryShader(true);
-            features.setMultiviewTessellationShader(true);
-            //                    features.setProtectedMemory(true);    // not supported
-            features.setSamplerYcbcrConversion(true);
-            features.setShaderDrawParameters(true);
-            features.setStorageBuffer16BitAccess(true);
-            //                    features.setStorageInputOutput16(true);   // not supported
-            features.setStoragePushConstant16(true);
-            features.setUniformAndStorageBuffer16BitAccess(true);
-            features.setVariablePointers(true);
-            features.setVariablePointersStorageBuffer(true);
-        });
-    m_features.requireFeature<vk::PhysicalDeviceAccelerationStructureFeaturesKHR>(
-        [](vk::PhysicalDeviceAccelerationStructureFeaturesKHR& features) {
-            features.setAccelerationStructure(true);
-            features.setAccelerationStructureCaptureReplay(true);
-            //                    features.setAccelerationStructureIndirectBuild(true); // not supported
-            //                    features.setAccelerationStructureHostCommands(true);  // not supported
-            features.setDescriptorBindingAccelerationStructureUpdateAfterBind(true);
-        });
+	
+	m_features.requireExtensionFeature<vk::PhysicalDeviceDescriptorIndexingFeatures>(
+			VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME,
+			[](vk::PhysicalDeviceDescriptorIndexingFeatures& features) {}
+	);
+	
+	m_features.requireExtensionFeature<vk::PhysicalDeviceBufferDeviceAddressFeatures>(
+			VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME,
+			[](vk::PhysicalDeviceBufferDeviceAddressFeatures& features) {
+				features.setBufferDeviceAddress(true);
+			}
+	);
+	
+    m_features.requireExtensionFeature<vk::PhysicalDeviceAccelerationStructureFeaturesKHR>(
+			VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME,
+			[](vk::PhysicalDeviceAccelerationStructureFeaturesKHR& features) {
+				features.setAccelerationStructure(true);
+			}
+	);
+	
     m_features.requireExtensionFeature<vk::PhysicalDeviceRayTracingPipelineFeaturesKHR>(
-        VK_KHR_RAY_TRACING_PIPELINE_EXTENSION_NAME, [](vk::PhysicalDeviceRayTracingPipelineFeaturesKHR& features) {
-            features.setRayTracingPipeline(true);
-            //                    features.setRayTracingPipelineShaderGroupHandleCaptureReplay(true);   // not supported
-            //                    features.setRayTracingPipelineShaderGroupHandleCaptureReplayMixed(true);  // not supported
-            features.setRayTracingPipelineTraceRaysIndirect(true);
-            features.setRayTraversalPrimitiveCulling(true);
-        });
+			VK_KHR_RAY_TRACING_PIPELINE_EXTENSION_NAME,
+			[](vk::PhysicalDeviceRayTracingPipelineFeaturesKHR& features) {
+				features.setRayTracingPipeline(true);
+			}
+	);
 }
 
 std::vector<const char*> RTXExtensions::getInstanceExtensions()
diff --git a/projects/rtx_ambient_occlusion/src/main.cpp b/projects/rtx_ambient_occlusion/src/main.cpp
index bac1fe367031e43cf2fc53d4be23c208f4fb682d..d4c8ec2cac8e26f70da346738da04de16a766a26 100644
--- a/projects/rtx_ambient_occlusion/src/main.cpp
+++ b/projects/rtx_ambient_occlusion/src/main.cpp
@@ -148,7 +148,7 @@ int main(int argc, const char** argv) {
 			rtxRegions.rmissRegion,
 			rtxRegions.rchitRegion,
 			rtxRegions.rcallRegion,
-			{	vkcv::DescriptorSetUsage(0, core.getDescriptorSet(rtxShaderDescriptorSet).vulkanHandle)},
+			{	vkcv::DescriptorSetUsage(0, rtxShaderDescriptorSet)},
 			pushConstantsRTX,
 			windowHandle);
 
diff --git a/projects/saf_r/src/main.cpp b/projects/saf_r/src/main.cpp
index c206fbffed7c9f5295fa0d5387ec5bfc9185d34a..ea5378406b092a92a1f8ee8149630f72059ca391 100644
--- a/projects/saf_r/src/main.cpp
+++ b/projects/saf_r/src/main.cpp
@@ -175,7 +175,7 @@ int main(int argc, const char** argv) {
 			(uint32_t)windowHeight,
 			safrPass,
 			{},
-			{ core.getDescriptorSetLayout(descriptorSetLayout).vulkanHandle },
+			{ descriptorSetLayout },
 			false
 	};
 
@@ -183,7 +183,7 @@ int main(int argc, const char** argv) {
 
 	const vkcv::ComputePipelineConfig computePipelineConfig{
 			computeShaderProgram,
-			{core.getDescriptorSetLayout(computeDescriptorSetLayout).vulkanHandle}
+			{computeDescriptorSetLayout}
 	};
 
 	vkcv::ComputePipelineHandle computePipeline = core.createComputePipeline(computePipelineConfig);
@@ -197,7 +197,7 @@ int main(int argc, const char** argv) {
 	auto start = std::chrono::system_clock::now();
 
 	const vkcv::Mesh renderMesh({}, safrIndexBuffer.getVulkanHandle(), 3);
-	vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle);
+	vkcv::DescriptorSetUsage descriptorUsage(0, descriptorSet);
 	vkcv::DrawcallInfo drawcall(renderMesh, { descriptorUsage }, 1);
 
 	//create the camera
@@ -279,7 +279,7 @@ int main(int argc, const char** argv) {
 		core.recordComputeDispatchToCmdStream(cmdStream,
 			computePipeline,
 			computeDispatchCount,
-			{ vkcv::DescriptorSetUsage(0,core.getDescriptorSet(computeDescriptorSet).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, computeDescriptorSet) },
 			pushConstantsCompute);
 
 		core.recordBufferMemoryBarrier(cmdStream, lightsBuffer.getHandle());
diff --git a/projects/sph/shaders/force.comp b/projects/sph/shaders/force.comp
index ea9b378b48a23fd0208ab18d884dbccda5ab21f4..694ddbdf7009d4e493a5f8dd8ba462e42d1e8b68 100644
--- a/projects/sph/shaders/force.comp
+++ b/projects/sph/shaders/force.comp
@@ -40,56 +40,113 @@ layout( push_constant ) uniform constants{
     float particleCount;
 };
 
-float spiky(float r)    
-{
-    return (15.f / (PI * pow(h, 6)) * pow((h-r), 3)) * int(0<=r && r<=h);
-}
-
 float grad_spiky(float r)
 {
-    return -45.f / (PI * pow(h, 6)) * pow((h-r), 2) * int(0<=r && r<=h);
+    return -45.f / (PI * pow(h, 6)) * pow((h-r), 2);
 }
 
-
-
 float laplacian(float r)
 {
-    return (45.f / (PI * pow(h,6)) * (h - r)) * int(0<=r && r<=h);
+    return (45.f / (PI * pow(h,6)) * (h - r));
 }
 
 vec3 pressureForce = vec3(0, 0, 0);
 vec3 viscosityForce = vec3(0, 0, 0); 
 vec3 externalForce = vec3(0, 0, 0);
 
-void main() {
+struct ParticleData
+{
+    vec3 position;
+    float density;
+    vec3 velocity;
+    float pressure;
+};
+
+shared ParticleData particle_data [256];
 
+void main() {
     uint id = gl_GlobalInvocationID.x;
 
-    if(id >= int(particleCount))
-    {
-        return;
+    if (id >= int(particleCount)) {
+        particle_data[gl_LocalInvocationIndex].position = vec3(0.0f);
+        particle_data[gl_LocalInvocationIndex].density  = 0.0f;
+        particle_data[gl_LocalInvocationIndex].velocity = vec3(0.0f);
+        particle_data[gl_LocalInvocationIndex].pressure = 0.0f;
+    } else {
+        particle_data[gl_LocalInvocationIndex].position = inParticle[id].position;
+        particle_data[gl_LocalInvocationIndex].density  = inParticle[id].density;
+        particle_data[gl_LocalInvocationIndex].velocity = inParticle[id].velocity;
+        particle_data[gl_LocalInvocationIndex].pressure = inParticle[id].pressure;
+    }
+
+    uint index_offset = gl_WorkGroupID.x * gl_WorkGroupSize.x;
+    uint group_size = min(index_offset + gl_WorkGroupSize.x, int(particleCount)) - index_offset;
+
+    memoryBarrierShared();
+    barrier();
+
+    const float h6 = pow(h, 6);
+    externalForce = particle_data[gl_LocalInvocationIndex].density * gravity * vec3(-gravityDir.x,gravityDir.y,gravityDir.z);
+
+    for(uint j = 1; j < group_size; j++) {
+        uint i = (gl_LocalInvocationIndex + j) % group_size;
+
+        vec3 dir = particle_data[gl_LocalInvocationIndex].position - particle_data[i].position;
+        float dist = length(dir);
+
+        if ((dist > 0.0f) && (dist <= h))
+        {
+            const float h_dist = (h - dist);
+
+            float laplacian = 45.f / (PI * h6) * h_dist;
+            float grad_spiky = -1.0f * laplacian * h_dist;
+
+            pressureForce += mass * -(particle_data[gl_LocalInvocationIndex].pressure + particle_data[i].pressure)/(2.f * particle_data[i].density) * grad_spiky * normalize(dir);
+            viscosityForce += mass * (particle_data[i].velocity - particle_data[gl_LocalInvocationIndex].velocity)/particle_data[i].density * laplacian;
+        }
     }
 
-    externalForce = inParticle[id].density * gravity * vec3(-gravityDir.x,gravityDir.y,gravityDir.z);
+    for(uint i = 0; i < index_offset; i++)
+    {
+        vec3 dir = particle_data[gl_LocalInvocationIndex].position - inParticle[i].position;
+        float dist = length(dir);
+
+        if ((dist > 0.0f) && (dist <= h))
+        {
+            const float h_dist = (h - dist);
 
-    for(uint i = 0; i < int(particleCount); i++)  
+            float laplacian = 45.f / (PI * h6) * h_dist;
+            float grad_spiky = -1.0f * laplacian * h_dist;
+
+            pressureForce += mass * -(particle_data[gl_LocalInvocationIndex].pressure + inParticle[i].pressure)/(2.f * inParticle[i].density) * grad_spiky * normalize(dir);
+            viscosityForce += mass * (inParticle[i].velocity - particle_data[gl_LocalInvocationIndex].velocity)/inParticle[i].density * laplacian;
+        }
+    }
+
+    for(uint i = index_offset + group_size; i < int(particleCount); i++)
     {
-        if (id != i)
+        vec3 dir = particle_data[gl_LocalInvocationIndex].position - inParticle[i].position;
+        float dist = length(dir);
+
+        if ((dist > 0.0f) && (dist <= h))
         {
-            vec3 dir = inParticle[id].position - inParticle[i].position;
-            float dist = length(dir);
-            if(dist != 0) 
-            {
-                pressureForce += mass * -(inParticle[id].pressure + inParticle[i].pressure)/(2.f * inParticle[i].density) * grad_spiky(dist) * normalize(dir);
-                viscosityForce += mass * (inParticle[i].velocity - inParticle[id].velocity)/inParticle[i].density * laplacian(dist);
-            }
+            const float h_dist = (h - dist);
+
+            float laplacian = 45.f / (PI * h6) * h_dist;
+            float grad_spiky = -1.0f * laplacian * h_dist;
+
+            pressureForce += mass * -(particle_data[gl_LocalInvocationIndex].pressure + inParticle[i].pressure)/(2.f * inParticle[i].density) * grad_spiky * normalize(dir);
+            viscosityForce += mass * (inParticle[i].velocity - particle_data[gl_LocalInvocationIndex].velocity)/inParticle[i].density * laplacian;
         }
     }
+
     viscosityForce *= viscosity;
 
-    outParticle[id].force = externalForce + pressureForce + viscosityForce;
-    outParticle[id].density = inParticle[id].density;
-    outParticle[id].pressure = inParticle[id].pressure;
-    outParticle[id].position = inParticle[id].position;
-    outParticle[id].velocity = inParticle[id].velocity;
+    if (id < int(particleCount)) {
+        outParticle[id].force = externalForce + pressureForce + viscosityForce;
+        outParticle[id].density = particle_data[gl_LocalInvocationIndex].density;
+        outParticle[id].pressure = particle_data[gl_LocalInvocationIndex].pressure;
+        outParticle[id].position = particle_data[gl_LocalInvocationIndex].position;
+        outParticle[id].velocity = particle_data[gl_LocalInvocationIndex].velocity;
+    }
 }
diff --git a/projects/sph/shaders/pressure.comp b/projects/sph/shaders/pressure.comp
index 05b3af3afb490b427cc1297f21a82a779d4c8ecb..8fa2e4762bddb3b9b28d8a3c184ceaaf7ab4421c 100644
--- a/projects/sph/shaders/pressure.comp
+++ b/projects/sph/shaders/pressure.comp
@@ -42,31 +42,53 @@ layout( push_constant ) uniform constants{
 
 float poly6(float r)    
 {
-    return (315.f * pow((pow(h,2)-pow(r,2)), 3)/(64.f*PI*pow(h, 9))) * int(0<=r && r<=h);
+    return (315.f * pow((pow(h,2)-pow(r,2)), 3)/(64.f*PI*pow(h, 9))) * int(r<=h);
 }
 
 float densitySum = 0.f;
 
+shared vec3 position_data [256];
+
 void main() {
     
     uint id = gl_GlobalInvocationID.x;
 
-    if(id >= int(particleCount))
+    if (id >= int(particleCount)) {
+        position_data[gl_LocalInvocationIndex] = vec3(0.0f);
+    } else {
+        position_data[gl_LocalInvocationIndex] = inParticle[id].position;
+    }
+
+    uint index_offset = gl_WorkGroupID.x * gl_WorkGroupSize.x;
+    uint group_size = min(index_offset + gl_WorkGroupSize.x, int(particleCount)) - index_offset;
+
+    memoryBarrierShared();
+    barrier();
+
+    for(uint j = 1; j < group_size; j++) {
+        uint i = (gl_LocalInvocationIndex + j) % group_size;
+
+        float dist = distance(position_data[gl_LocalInvocationIndex], position_data[i]);
+        densitySum += mass * poly6(dist);
+    }
+
+    for(uint i = 0; i < index_offset; i++)
     {
-        return;
+        float dist = distance(position_data[gl_LocalInvocationIndex], inParticle[i].position);
+        densitySum += mass * poly6(dist);
     }
 
-    for(uint i = 0; i < int(particleCount); i++)   
+    for(uint i = index_offset + group_size; i < int(particleCount); i++)
     {
-        if (id != i)
-        {
-            float dist = distance(inParticle[id].position, inParticle[i].position);
-            densitySum += mass * poly6(dist);
-        }
+        float dist = distance(position_data[gl_LocalInvocationIndex], inParticle[i].position);
+        densitySum += mass * poly6(dist);
+    }
+
+    if (id < int(particleCount)) {
+        outParticle[id].density = max(densitySum, 0.0000001f);
+        outParticle[id].pressure = max((densitySum - offset), 0.0000001f) * gasConstant;
+        outParticle[id].position = position_data[gl_LocalInvocationIndex];
+        outParticle[id].velocity = inParticle[id].velocity;
+        outParticle[id].force = inParticle[id].force;
     }
-    outParticle[id].density = max(densitySum,0.0000001f);
-    outParticle[id].pressure = max((densitySum - offset), 0.0000001f) * gasConstant;
-    outParticle[id].position = inParticle[id].position;
-    outParticle[id].velocity = inParticle[id].velocity;
-    outParticle[id].force = inParticle[id].force;
 }
diff --git a/projects/sph/src/BloomAndFlares.cpp b/projects/sph/src/BloomAndFlares.cpp
index 09534815afcd8ab238b79da5c6bbceb6672b043a..200c0dea16a0b1483a8b20786902b38a43b5f825 100644
--- a/projects/sph/src/BloomAndFlares.cpp
+++ b/projects/sph/src/BloomAndFlares.cpp
@@ -37,7 +37,8 @@ BloomAndFlares::BloomAndFlares(
 		        p_Core->createDescriptorSet(m_DownsampleDescSetLayouts.back()));
     }
     m_DownsamplePipe = p_Core->createComputePipeline({
-            dsProg, { p_Core->getDescriptorSetLayout(m_DownsampleDescSetLayouts[0]).vulkanHandle } });
+            dsProg, m_DownsampleDescSetLayouts
+	});
 
     // UPSAMPLE
     vkcv::ShaderProgram usProg;
@@ -55,7 +56,8 @@ BloomAndFlares::BloomAndFlares(
                 p_Core->createDescriptorSet(m_UpsampleDescSetLayouts.back()));
     }
     m_UpsamplePipe = p_Core->createComputePipeline({
-            usProg, { p_Core->getDescriptorSetLayout(m_UpsampleDescSetLayouts[0]).vulkanHandle } });
+            usProg, m_UpsampleDescSetLayouts
+	});
 
     // LENS FEATURES
     vkcv::ShaderProgram lensProg;
@@ -68,7 +70,8 @@ BloomAndFlares::BloomAndFlares(
     m_LensFlareDescSetLayout = p_Core->createDescriptorSetLayout(lensProg.getReflectedDescriptors().at(0));
     m_LensFlareDescSet = p_Core->createDescriptorSet(m_LensFlareDescSetLayout);
     m_LensFlarePipe = p_Core->createComputePipeline({
-            lensProg, { p_Core->getDescriptorSetLayout(m_LensFlareDescSetLayout).vulkanHandle } });
+            lensProg, { m_LensFlareDescSetLayout }
+	});
 
     // COMPOSITE
     vkcv::ShaderProgram compProg;
@@ -81,7 +84,8 @@ BloomAndFlares::BloomAndFlares(
     m_CompositeDescSetLayout = p_Core->createDescriptorSetLayout(compProg.getReflectedDescriptors().at(0));
     m_CompositeDescSet = p_Core->createDescriptorSet(m_CompositeDescSetLayout);
     m_CompositePipe = p_Core->createComputePipeline({
-            compProg, { p_Core->getDescriptorSetLayout(m_CompositeDescSetLayout).vulkanHandle } });
+            compProg, { m_CompositeDescSetLayout }
+	});
 }
 
 void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStream,
@@ -110,7 +114,7 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
             cmdStream,
             m_DownsamplePipe,
             initialDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[0]).vulkanHandle)},
+            {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[0])},
             vkcv::PushConstants(0));
 
     // downsample dispatches of blur buffer's mip maps
@@ -145,7 +149,7 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
                 cmdStream,
                 m_DownsamplePipe,
                 mipDispatchCount,
-                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[mipLevel]).vulkanHandle)},
+                {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[mipLevel])},
                 vkcv::PushConstants(0));
 
         // image barrier between mips
@@ -190,7 +194,7 @@ void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream
                 cmdStream,
                 m_UpsamplePipe,
                 upsampleDispatchCount,
-                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_UpsampleDescSets[mipLevel]).vulkanHandle)},
+                {vkcv::DescriptorSetUsage(0, m_UpsampleDescSets[mipLevel])},
                 vkcv::PushConstants(0)
         );
         // image barrier between mips
@@ -222,7 +226,7 @@ void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStr
             cmdStream,
             m_LensFlarePipe,
             lensFeatureDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_LensFlareDescSet).vulkanHandle)},
+            {vkcv::DescriptorSetUsage(0, m_LensFlareDescSet)},
             vkcv::PushConstants(0));
 }
 
@@ -255,7 +259,7 @@ void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStrea
             cmdStream,
             m_CompositePipe,
             compositeDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_CompositeDescSet).vulkanHandle)},
+            {vkcv::DescriptorSetUsage(0, m_CompositeDescSet)},
             vkcv::PushConstants(0));
 }
 
diff --git a/projects/sph/src/PipelineInit.cpp b/projects/sph/src/PipelineInit.cpp
index 6cf941fa0d8f8716b7d05daf9b6fb618b0fa7d85..052c983cb094114853d19dac3e76149db99116db 100644
--- a/projects/sph/src/PipelineInit.cpp
+++ b/projects/sph/src/PipelineInit.cpp
@@ -21,7 +21,8 @@ vkcv::DescriptorSetHandle PipelineInit::ComputePipelineInit(vkcv::Core *pCore, v
 
     pipeline = pCore->createComputePipeline({
             shaderProgram,
-            { pCore->getDescriptorSetLayout(descriptorSetLayout).vulkanHandle } });
+            { descriptorSetLayout }
+	});
 
     return  descriptorSet;
 }
\ No newline at end of file
diff --git a/projects/sph/src/main.cpp b/projects/sph/src/main.cpp
index 738d7699b4bcfa34ce70ca19864a3eb9724c7e27..c3305fdd335115ef108ca87a8065da3f5302e7a0 100644
--- a/projects/sph/src/main.cpp
+++ b/projects/sph/src/main.cpp
@@ -122,8 +122,9 @@ int main(int argc, const char **argv) {
             UINT32_MAX,
             particlePass,
             {particleLayout},
-            {core.getDescriptorSetLayout(descriptorSetLayout).vulkanHandle},
-            true};
+            {descriptorSetLayout},
+            true
+	};
     particlePipelineDefinition.m_blendMode = vkcv::BlendMode::Additive;
 
     const std::vector<glm::vec3> vertices = {glm::vec3(-0.012, 0.012, 0),
@@ -206,7 +207,7 @@ int main(int argc, const char **argv) {
 
     const vkcv::Mesh renderMesh({vertexBufferBindings}, particleIndexBuffer.getVulkanHandle(),
                                 particleIndexBuffer.getCount());
-    vkcv::DescriptorSetUsage descriptorUsage(0, core.getDescriptorSet(descriptorSet).vulkanHandle);
+    vkcv::DescriptorSetUsage descriptorUsage(0, descriptorSet);
 
     auto pos = glm::vec2(0.f);
 
@@ -332,7 +333,7 @@ int main(int argc, const char **argv) {
         core.recordComputeDispatchToCmdStream(cmdStream,
                                               pressurePipeline,
                                               computeDispatchCount,
-                                              {vkcv::DescriptorSetUsage(0,core.getDescriptorSet(pressureDescriptorSet).vulkanHandle)},
+                                              {vkcv::DescriptorSetUsage(0, pressureDescriptorSet)},
 											  pushConstantsCompute);
 
         core.recordBufferMemoryBarrier(cmdStream, particleBuffer1.getHandle());
@@ -340,10 +341,10 @@ int main(int argc, const char **argv) {
 
         // computing force pipeline
 		core.recordComputeDispatchToCmdStream(cmdStream,
-                                              forcePipeline,
-                                              computeDispatchCount,
-                                              {vkcv::DescriptorSetUsage(0,core.getDescriptorSet(forceDescriptorSet).vulkanHandle)},
-                                              pushConstantsCompute);
+											  forcePipeline,
+											  computeDispatchCount,
+											  {vkcv::DescriptorSetUsage(0, forceDescriptorSet)},
+											  pushConstantsCompute);
 
 		core.recordBufferMemoryBarrier(cmdStream, particleBuffer1.getHandle());
 		core.recordBufferMemoryBarrier(cmdStream, particleBuffer2.getHandle());
@@ -352,7 +353,7 @@ int main(int argc, const char **argv) {
         core.recordComputeDispatchToCmdStream(cmdStream,
                                               updateDataPipeline,
                                               computeDispatchCount,
-                                              { vkcv::DescriptorSetUsage(0,core.getDescriptorSet(updateDataDescriptorSet).vulkanHandle) },
+                                              { vkcv::DescriptorSetUsage(0, updateDataDescriptorSet) },
                                               pushConstantsCompute);
 
         core.recordBufferMemoryBarrier(cmdStream, particleBuffer1.getHandle());
@@ -362,7 +363,7 @@ int main(int argc, const char **argv) {
         core.recordComputeDispatchToCmdStream(cmdStream,
                                               flipPipeline,
                                               computeDispatchCount,
-                                              { vkcv::DescriptorSetUsage(0,core.getDescriptorSet(flipDescriptorSet).vulkanHandle) },
+                                              { vkcv::DescriptorSetUsage(0, flipDescriptorSet) },
                                               pushConstantsCompute);
 
         core.recordBufferMemoryBarrier(cmdStream, particleBuffer1.getHandle());
@@ -403,7 +404,7 @@ int main(int argc, const char **argv) {
             cmdStream, 
             tonemappingPipe, 
             tonemappingDispatchCount, 
-            {vkcv::DescriptorSetUsage(0, core.getDescriptorSet(tonemappingDescriptor).vulkanHandle) },
+            {vkcv::DescriptorSetUsage(0, tonemappingDescriptor) },
             vkcv::PushConstants(0));
 
         core.prepareSwapchainImageForPresent(cmdStream);
diff --git a/projects/voxelization/CMakeLists.txt b/projects/voxelization/CMakeLists.txt
index 9597f9493b91ef48a5932fb22da7dce791dc450c..ba3c467766377d22925ac9c90acffee7fe324332 100644
--- a/projects/voxelization/CMakeLists.txt
+++ b/projects/voxelization/CMakeLists.txt
@@ -19,15 +19,7 @@ target_sources(voxelization PRIVATE
     src/BloomAndFlares.hpp
     src/BloomAndFlares.cpp)
 
-# this should fix the execution path to load local files from the project (for MSVC)
-if(MSVC)
-	set_target_properties(voxelization PROPERTIES RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-	set_target_properties(voxelization PROPERTIES RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-    
-    # in addition to setting the output directory, the working directory has to be set
-	# by default visual studio sets the working directory to the build directory, when using the debugger
-	set_target_properties(voxelization PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
-endif()
+fix_project(voxelization)
 
 # including headers of dependencies and the VkCV framework
 target_include_directories(voxelization SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_asset_loader_include} ${vkcv_camera_include} ${vkcv_shader_compiler_include} ${vkcv_gui_include} ${vkcv_upscaling_include})
diff --git a/projects/voxelization/src/BloomAndFlares.cpp b/projects/voxelization/src/BloomAndFlares.cpp
index 2014d7a0219141ec6363b38a5311cb924b6b6a45..ddb1326ae83c8bd596ce61dc1c47b81b5ddb17be 100644
--- a/projects/voxelization/src/BloomAndFlares.cpp
+++ b/projects/voxelization/src/BloomAndFlares.cpp
@@ -51,7 +51,7 @@ BloomAndFlares::BloomAndFlares(
     }
 
     m_DownsamplePipe = p_Core->createComputePipeline({
-        dsProg, { p_Core->getDescriptorSetLayout(m_DownsampleDescSetLayouts[0]).vulkanHandle }
+        dsProg, m_DownsampleDescSetLayouts
     });
 
     // UPSAMPLE
@@ -77,7 +77,7 @@ BloomAndFlares::BloomAndFlares(
     }
 
     m_UpsamplePipe = p_Core->createComputePipeline({
-        usProg, { p_Core->getDescriptorSetLayout(m_UpsampleDescSetLayouts[0]).vulkanHandle }
+        usProg, m_UpsampleDescSetLayouts
     });
 
     // LENS FEATURES
@@ -92,7 +92,8 @@ BloomAndFlares::BloomAndFlares(
     m_LensFlareDescSetLayout = p_Core->createDescriptorSetLayout(lensProg.getReflectedDescriptors().at(0));
     m_LensFlareDescSet = p_Core->createDescriptorSet(m_LensFlareDescSetLayout);
     m_LensFlarePipe = p_Core->createComputePipeline(
-        { lensProg, { p_Core->getDescriptorSetLayout(m_LensFlareDescSetLayout).vulkanHandle } });
+        { lensProg, { m_LensFlareDescSetLayout }
+	});
 
 
     // COMPOSITE
@@ -107,7 +108,8 @@ BloomAndFlares::BloomAndFlares(
     m_CompositeDescSetLayout = p_Core->createDescriptorSetLayout(compProg.getReflectedDescriptors().at(0));
     m_CompositeDescSet = p_Core->createDescriptorSet(m_CompositeDescSetLayout);
     m_CompositePipe = p_Core->createComputePipeline(
-        { compProg, { p_Core->getDescriptorSetLayout(m_CompositeDescSetLayout).vulkanHandle } });
+        { compProg, { m_CompositeDescSetLayout }
+	});
 
     // radial LUT
     const auto texture = vkcv::asset::loadTexture("assets/RadialLUT.png");
@@ -143,7 +145,7 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
             cmdStream,
             m_DownsamplePipe,
             initialDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[0]).vulkanHandle)},
+            {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[0])},
             vkcv::PushConstants(0));
 
     // downsample dispatches of blur buffer's mip maps
@@ -178,7 +180,7 @@ void BloomAndFlares::execDownsamplePipe(const vkcv::CommandStreamHandle &cmdStre
                 cmdStream,
                 m_DownsamplePipe,
                 mipDispatchCount,
-                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_DownsampleDescSets[mipLevel]).vulkanHandle)},
+                {vkcv::DescriptorSetUsage(0, m_DownsampleDescSets[mipLevel])},
                 vkcv::PushConstants(0));
 
         // image barrier between mips
@@ -227,7 +229,7 @@ void BloomAndFlares::execUpsamplePipe(const vkcv::CommandStreamHandle &cmdStream
                 cmdStream,
                 m_UpsamplePipe,
                 upsampleDispatchCount,
-                {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_UpsampleDescSets[mipLevel]).vulkanHandle)},
+                {vkcv::DescriptorSetUsage(0, m_UpsampleDescSets[mipLevel])},
                 vkcv::PushConstants(0)
         );
         // image barrier between mips
@@ -266,7 +268,7 @@ void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStr
             cmdStream,
             m_LensFlarePipe,
             lensFeatureDispatchCount,
-            {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_LensFlareDescSet).vulkanHandle)},
+            {vkcv::DescriptorSetUsage(0, m_LensFlareDescSet)},
             vkcv::PushConstants(0));
 
     // upsample dispatch
@@ -299,7 +301,7 @@ void BloomAndFlares::execLensFeaturePipe(const vkcv::CommandStreamHandle &cmdStr
             cmdStream,
             m_UpsamplePipe,
             upsampleDispatchCount,
-            { vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_UpsampleLensFlareDescSets[i]).vulkanHandle) },
+            { vkcv::DescriptorSetUsage(0, m_UpsampleLensFlareDescSets[i]) },
             vkcv::PushConstants(0)
         );
         // image barrier between mips
@@ -346,7 +348,7 @@ void BloomAndFlares::execCompositePipe(const vkcv::CommandStreamHandle &cmdStrea
         cmdStream,
         m_CompositePipe,
         compositeDispatchCount,
-        {vkcv::DescriptorSetUsage(0, p_Core->getDescriptorSet(m_CompositeDescSet).vulkanHandle)},
+        {vkcv::DescriptorSetUsage(0, m_CompositeDescSet)},
         pushConstants);
 
     p_Core->recordEndDebugLabel(cmdStream);
diff --git a/projects/voxelization/src/ShadowMapping.cpp b/projects/voxelization/src/ShadowMapping.cpp
index ce4261ff2403139d10b9d677e7aa216a3e41178f..5ae7eb6047200b2cdb7a3ac38ce8512cdcaa3d53 100644
--- a/projects/voxelization/src/ShadowMapping.cpp
+++ b/projects/voxelization/src/ShadowMapping.cpp
@@ -192,7 +192,7 @@ ShadowMapping::ShadowMapping(vkcv::Core* corePtr, const vkcv::VertexLayout& vert
 
 	m_depthToMomentsDescriptorSetLayout         = corePtr->createDescriptorSetLayout(depthToMomentsShader.getReflectedDescriptors().at(0));
 	m_depthToMomentsDescriptorSet               = corePtr->createDescriptorSet(m_depthToMomentsDescriptorSetLayout);
-    m_depthToMomentsPipe = corePtr->createComputePipeline({ depthToMomentsShader, { corePtr->getDescriptorSetLayout(m_depthToMomentsDescriptorSetLayout).vulkanHandle }});
+    m_depthToMomentsPipe = corePtr->createComputePipeline({ depthToMomentsShader, { m_depthToMomentsDescriptorSetLayout }});
 
 	vkcv::DescriptorWrites depthToMomentDescriptorWrites;
 	depthToMomentDescriptorWrites.sampledImageWrites    = { vkcv::SampledImageDescriptorWrite(0, m_shadowMapDepth.getHandle()) };
@@ -204,7 +204,7 @@ ShadowMapping::ShadowMapping(vkcv::Core* corePtr, const vkcv::VertexLayout& vert
 	vkcv::ShaderProgram shadowBlurXShader   = loadShadowBlurXShader();
 	m_shadowBlurXDescriptorSetLayout        = corePtr->createDescriptorSetLayout(shadowBlurXShader.getReflectedDescriptors().at(0));
 	m_shadowBlurXDescriptorSet              = corePtr->createDescriptorSet(m_shadowBlurXDescriptorSetLayout);
-	m_shadowBlurXPipe                       = corePtr->createComputePipeline({ shadowBlurXShader, { corePtr->getDescriptorSetLayout(m_shadowBlurXDescriptorSetLayout).vulkanHandle }});
+	m_shadowBlurXPipe                       = corePtr->createComputePipeline({ shadowBlurXShader, { m_shadowBlurXDescriptorSetLayout }});
 
 	vkcv::DescriptorWrites shadowBlurXDescriptorWrites;
 	shadowBlurXDescriptorWrites.sampledImageWrites   = { vkcv::SampledImageDescriptorWrite(0, m_shadowMap.getHandle()) };
@@ -216,7 +216,7 @@ ShadowMapping::ShadowMapping(vkcv::Core* corePtr, const vkcv::VertexLayout& vert
 	vkcv::ShaderProgram shadowBlurYShader   = loadShadowBlurYShader();
 	m_shadowBlurYDescriptorSetLayout        = corePtr->createDescriptorSetLayout(shadowBlurYShader.getReflectedDescriptors().at(0));
 	m_shadowBlurYDescriptorSet              = corePtr->createDescriptorSet(m_shadowBlurYDescriptorSetLayout);
-    m_shadowBlurYPipe                       = corePtr->createComputePipeline({ shadowBlurYShader, { corePtr->getDescriptorSetLayout(m_shadowBlurYDescriptorSetLayout).vulkanHandle }});
+    m_shadowBlurYPipe                       = corePtr->createComputePipeline({ shadowBlurYShader, { m_shadowBlurYDescriptorSetLayout }});
 
     vkcv::DescriptorWrites shadowBlurYDescriptorWrites;
 	shadowBlurYDescriptorWrites.sampledImageWrites  = { vkcv::SampledImageDescriptorWrite(0, m_shadowMapIntermediate.getHandle()) };
@@ -296,7 +296,7 @@ void ShadowMapping::recordShadowMapRendering(
 		cmdStream,
 		m_depthToMomentsPipe,
 		dispatchCount,
-		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_depthToMomentsDescriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_depthToMomentsDescriptorSet) },
 		msaaPushConstants);
 	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMap.getHandle());
 	m_corePtr->recordEndDebugLabel(cmdStream);
@@ -309,7 +309,7 @@ void ShadowMapping::recordShadowMapRendering(
 		cmdStream,
 		m_shadowBlurXPipe,
 		dispatchCount,
-		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_shadowBlurXDescriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_shadowBlurXDescriptorSet) },
 		vkcv::PushConstants(0));
 	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMapIntermediate.getHandle());
 
@@ -319,7 +319,7 @@ void ShadowMapping::recordShadowMapRendering(
 		cmdStream,
 		m_shadowBlurYPipe,
 		dispatchCount,
-		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_shadowBlurYDescriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_shadowBlurYDescriptorSet) },
 		vkcv::PushConstants(0));
 	m_shadowMap.recordMipChainGeneration(cmdStream);
 	m_corePtr->prepareImageForSampling(cmdStream, m_shadowMap.getHandle());
diff --git a/projects/voxelization/src/Voxelization.cpp b/projects/voxelization/src/Voxelization.cpp
index c023af21c673651984e945ab03d16280c18f0768..faa03d38127d5a23c931fdef0470c1e24131d206 100644
--- a/projects/voxelization/src/Voxelization.cpp
+++ b/projects/voxelization/src/Voxelization.cpp
@@ -110,11 +110,10 @@ Voxelization::Voxelization(
 		voxelResolution,
 		m_voxelizationPass,
 		dependencies.vertexLayout,
-		{ 
-		    m_corePtr->getDescriptorSetLayout(m_voxelizationDescriptorSetLayout).vulkanHandle,
-		    m_corePtr->getDescriptorSetLayout(dummyPerMeshDescriptorSetLayout).vulkanHandle},
+		{ m_voxelizationDescriptorSetLayout, dummyPerMeshDescriptorSetLayout },
 		false,
-		true };
+		true
+	};
 	m_voxelizationPipe = m_corePtr->createGraphicsPipeline(voxelizationPipeConfig);
 
 	vkcv::DescriptorWrites voxelizationDescriptorWrites;
@@ -156,10 +155,11 @@ Voxelization::Voxelization(
 		0,
 		m_visualisationPass,
 		{},
-		{ m_corePtr->getDescriptorSetLayout(m_visualisationDescriptorSetLayout).vulkanHandle },
+		{ m_visualisationDescriptorSetLayout },
 		true,
 		false,
-		vkcv::PrimitiveTopology::PointList };	// points are extended to cubes in the geometry shader
+		vkcv::PrimitiveTopology::PointList
+	};	// points are extended to cubes in the geometry shader
 	voxelVisualisationPipeConfig.m_multisampling = msaa;
 	m_visualisationPipe = m_corePtr->createGraphicsPipeline(voxelVisualisationPipeConfig);
 
@@ -168,7 +168,7 @@ Voxelization::Voxelization(
 		voxelIndexData.push_back(static_cast<uint16_t>(i));
 	}
 
-	const vkcv::DescriptorSetUsage voxelizationDescriptorUsage(0, m_corePtr->getDescriptorSet(m_visualisationDescriptorSet).vulkanHandle);
+	const vkcv::DescriptorSetUsage voxelizationDescriptorUsage(0, m_visualisationDescriptorSet);
 
 	vkcv::ShaderProgram resetVoxelShader = loadVoxelResetShader();
 
@@ -176,7 +176,8 @@ Voxelization::Voxelization(
 	m_voxelResetDescriptorSet = m_corePtr->createDescriptorSet(m_voxelResetDescriptorSetLayout);
 	m_voxelResetPipe = m_corePtr->createComputePipeline({
 		resetVoxelShader,
-		{ m_corePtr->getDescriptorSetLayout(m_voxelResetDescriptorSetLayout).vulkanHandle }});
+		{ m_voxelResetDescriptorSetLayout }
+	});
 
 	vkcv::DescriptorWrites resetVoxelWrites;
 	resetVoxelWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
@@ -189,7 +190,8 @@ Voxelization::Voxelization(
 	m_bufferToImageDescriptorSet = m_corePtr->createDescriptorSet(m_bufferToImageDescriptorSetLayout);
 	m_bufferToImagePipe = m_corePtr->createComputePipeline({
 		bufferToImageShader,
-		{ m_corePtr->getDescriptorSetLayout(m_bufferToImageDescriptorSetLayout).vulkanHandle }});
+		{ m_bufferToImageDescriptorSetLayout }
+	});
 
 	vkcv::DescriptorWrites bufferToImageDescriptorWrites;
 	bufferToImageDescriptorWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
@@ -203,7 +205,8 @@ Voxelization::Voxelization(
 	m_secondaryBounceDescriptorSet = m_corePtr->createDescriptorSet(m_secondaryBounceDescriptorSetLayout);
 	m_secondaryBouncePipe = m_corePtr->createComputePipeline({
 		secondaryBounceShader,
-		{ m_corePtr->getDescriptorSetLayout(m_secondaryBounceDescriptorSetLayout).vulkanHandle }});
+		{ m_secondaryBounceDescriptorSetLayout }
+	});
 
 	vkcv::DescriptorWrites secondaryBounceDescriptorWrites;
 	secondaryBounceDescriptorWrites.storageBufferWrites = { vkcv::BufferDescriptorWrite(0, m_voxelBuffer.getHandle()) };
@@ -256,7 +259,7 @@ void Voxelization::voxelizeMeshes(
 		cmdStream,
 		m_voxelResetPipe,
 		resetVoxelDispatchCount,
-		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_voxelResetDescriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_voxelResetDescriptorSet) },
 		voxelCountPushConstants);
 	m_corePtr->recordBufferMemoryBarrier(cmdStream, m_voxelBuffer.getHandle());
 	m_corePtr->recordEndDebugLabel(cmdStream);
@@ -267,8 +270,8 @@ void Voxelization::voxelizeMeshes(
 		drawcalls.push_back(vkcv::DrawcallInfo(
 			meshes[i],
 			{ 
-				vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_voxelizationDescriptorSet).vulkanHandle),
-				vkcv::DescriptorSetUsage(1, m_corePtr->getDescriptorSet(perMeshDescriptorSets[i]).vulkanHandle) 
+				vkcv::DescriptorSetUsage(0, m_voxelizationDescriptorSet),
+				vkcv::DescriptorSetUsage(1, perMeshDescriptorSets[i])
 			},1));
 	}
 
@@ -296,7 +299,7 @@ void Voxelization::voxelizeMeshes(
 		cmdStream,
 		m_bufferToImagePipe,
 		bufferToImageDispatchCount,
-		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_bufferToImageDescriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_bufferToImageDescriptorSet) },
 		vkcv::PushConstants(0));
 
 	m_corePtr->recordImageMemoryBarrier(cmdStream, m_voxelImageIntermediate.getHandle());
@@ -315,7 +318,7 @@ void Voxelization::voxelizeMeshes(
 		cmdStream,
 		m_secondaryBouncePipe,
 		bufferToImageDispatchCount,
-		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_secondaryBounceDescriptorSet).vulkanHandle) },
+		{ vkcv::DescriptorSetUsage(0, m_secondaryBounceDescriptorSet) },
 		vkcv::PushConstants(0));
 	m_voxelImage.recordMipChainGeneration(cmdStream);
 	m_corePtr->recordImageMemoryBarrier(cmdStream, m_voxelImage.getHandle());
@@ -352,7 +355,7 @@ void Voxelization::renderVoxelVisualisation(
 
 	const auto drawcall = vkcv::DrawcallInfo(
 		vkcv::Mesh({}, nullptr, drawVoxelCount),
-		{ vkcv::DescriptorSetUsage(0, m_corePtr->getDescriptorSet(m_visualisationDescriptorSet).vulkanHandle) },1);
+		{ vkcv::DescriptorSetUsage(0, m_visualisationDescriptorSet) },1);
 
 	m_corePtr->recordBeginDebugLabel(cmdStream, "Voxel visualisation", { 1, 1, 1, 1 });
 	m_corePtr->prepareImageForStorage(cmdStream, m_voxelImage.getHandle());
diff --git a/projects/voxelization/src/main.cpp b/projects/voxelization/src/main.cpp
index a4ffb668e74d0a0829bb3c436ed5b992695b6ecf..2245419f87d196e913ba27e8e78ffff73aab04b6 100644
--- a/projects/voxelization/src/main.cpp
+++ b/projects/voxelization/src/main.cpp
@@ -317,10 +317,9 @@ int main(int argc, const char** argv) {
 		swapchainExtent.height,
 		prepassPass,
 		vertexLayout,
-		{ 
-		    core.getDescriptorSetLayout(prepassDescriptorSetLayout).vulkanHandle,
-			core.getDescriptorSetLayout(perMeshDescriptorSetLayouts[0]).vulkanHandle },
-		true };
+		{ prepassDescriptorSetLayout, perMeshDescriptorSetLayouts[0] },
+		true
+	};
 	prepassPipelineConfig.m_culling         = vkcv::CullMode::Back;
 	prepassPipelineConfig.m_multisampling   = msaa;
 	prepassPipelineConfig.m_depthTest       = vkcv::DepthTest::LessEqual;
@@ -335,9 +334,7 @@ int main(int argc, const char** argv) {
 		swapchainExtent.height,
 		forwardPass,
 		vertexLayout,
-		{	
-		    core.getDescriptorSetLayout(forwardShadingDescriptorSetLayout).vulkanHandle,
-			core.getDescriptorSetLayout(perMeshDescriptorSetLayouts[0]).vulkanHandle },
+		{ forwardShadingDescriptorSetLayout, perMeshDescriptorSetLayouts[0] },
 		true
 	};
     forwardPipelineConfig.m_culling         = vkcv::CullMode::Back;
@@ -468,7 +465,8 @@ int main(int argc, const char** argv) {
 	vkcv::DescriptorSetHandle tonemappingDescriptorSet = core.createDescriptorSet(tonemappingDescriptorSetLayout);
 	vkcv::ComputePipelineHandle tonemappingPipeline = core.createComputePipeline({
 		tonemappingProgram,
-		{ core.getDescriptorSetLayout(tonemappingDescriptorSetLayout).vulkanHandle }});
+		{ tonemappingDescriptorSetLayout }
+	});
 	
 	// tonemapping compute shader
 	vkcv::ShaderProgram postEffectsProgram;
@@ -482,7 +480,8 @@ int main(int argc, const char** argv) {
 	vkcv::DescriptorSetHandle postEffectsDescriptorSet = core.createDescriptorSet(postEffectsDescriptorSetLayout);
 	vkcv::ComputePipelineHandle postEffectsPipeline = core.createComputePipeline({
 			postEffectsProgram,
-			{ core.getDescriptorSetLayout(postEffectsDescriptorSetLayout).vulkanHandle }});
+			{ postEffectsDescriptorSetLayout }
+	});
 
 	// resolve compute shader
 	vkcv::ShaderProgram resolveProgram;
@@ -496,7 +495,8 @@ int main(int argc, const char** argv) {
 	vkcv::DescriptorSetHandle resolveDescriptorSet = core.createDescriptorSet(resolveDescriptorSetLayout);
 	vkcv::ComputePipelineHandle resolvePipeline = core.createComputePipeline({
 		resolveProgram,
-		{ core.getDescriptorSetLayout(resolveDescriptorSetLayout).vulkanHandle }});
+		{ resolveDescriptorSetLayout }
+	});
 
 	vkcv::SamplerHandle resolveSampler = core.createSampler(
 		vkcv::SamplerFilterType::NEAREST,
@@ -527,11 +527,11 @@ int main(int argc, const char** argv) {
 	for (size_t i = 0; i < meshes.size(); i++) {
 
 		drawcalls.push_back(vkcv::DrawcallInfo(meshes[i], { 
-			vkcv::DescriptorSetUsage(0, core.getDescriptorSet(forwardShadingDescriptorSet).vulkanHandle),
-			vkcv::DescriptorSetUsage(1, core.getDescriptorSet(perMeshDescriptorSets[i]).vulkanHandle) }));
+			vkcv::DescriptorSetUsage(0, forwardShadingDescriptorSet),
+			vkcv::DescriptorSetUsage(1, perMeshDescriptorSets[i]) }));
 		prepassDrawcalls.push_back(vkcv::DrawcallInfo(meshes[i], {
-			vkcv::DescriptorSetUsage(0, core.getDescriptorSet(prepassDescriptorSet).vulkanHandle),
-			vkcv::DescriptorSetUsage(1, core.getDescriptorSet(perMeshDescriptorSets[i]).vulkanHandle) }));
+			vkcv::DescriptorSetUsage(0, prepassDescriptorSet),
+			vkcv::DescriptorSetUsage(1, perMeshDescriptorSets[i]) }));
 	}
 
 	vkcv::SamplerHandle voxelSampler = core.createSampler(
@@ -859,7 +859,7 @@ int main(int argc, const char** argv) {
 					cmdStream,
 					resolvePipeline,
 					fulsscreenDispatchCount,
-					{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(resolveDescriptorSet).vulkanHandle) },
+					{ vkcv::DescriptorSetUsage(0, resolveDescriptorSet) },
 					vkcv::PushConstants(0));
 
 				core.recordImageMemoryBarrier(cmdStream, resolvedColorBuffer);
@@ -882,9 +882,7 @@ int main(int argc, const char** argv) {
 			cmdStream, 
 			tonemappingPipeline, 
 			fulsscreenDispatchCount,
-			{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(
-					tonemappingDescriptorSet
-			).vulkanHandle) },
+			{ vkcv::DescriptorSetUsage(0, tonemappingDescriptorSet) },
 			vkcv::PushConstants(0)
 		);
 		
@@ -920,9 +918,7 @@ int main(int argc, const char** argv) {
 				cmdStream,
 				postEffectsPipeline,
 				fulsscreenDispatchCount,
-				{ vkcv::DescriptorSetUsage(0, core.getDescriptorSet(
-						postEffectsDescriptorSet
-				).vulkanHandle) },
+				{ vkcv::DescriptorSetUsage(0, postEffectsDescriptorSet) },
 				timePushConstants
 		);
 		core.recordEndDebugLabel(cmdStream);
@@ -1001,7 +997,8 @@ int main(int argc, const char** argv) {
 
 				vkcv::ComputePipelineHandle newPipeline = core.createComputePipeline({
 					newProgram,
-					{ core.getDescriptorSetLayout(tonemappingDescriptorSetLayout).vulkanHandle }});
+					{ tonemappingDescriptorSetLayout }
+				});
 
 				if (newPipeline) {
 					tonemappingPipeline = newPipeline;
diff --git a/projects/wobble_bobble/.gitignore b/projects/wobble_bobble/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..7ed07ed409373206a95d74e6dbdc27a4cb391fea
--- /dev/null
+++ b/projects/wobble_bobble/.gitignore
@@ -0,0 +1 @@
+wobble_bobble
diff --git a/projects/wobble_bobble/CMakeLists.txt b/projects/wobble_bobble/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..9842b13b1038d61414de5c2cf2a9107604b6fe16
--- /dev/null
+++ b/projects/wobble_bobble/CMakeLists.txt
@@ -0,0 +1,21 @@
+cmake_minimum_required(VERSION 3.16)
+project(wobble_bobble)
+
+# setting c++ standard for the project
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+
+# this should fix the execution path to load local files from the project
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
+
+# adding source files to the project
+add_executable(wobble_bobble
+		src/main.cpp)
+
+fix_project(wobble_bobble)
+
+# including headers of dependencies and the VkCV framework
+target_include_directories(wobble_bobble SYSTEM BEFORE PRIVATE ${vkcv_include} ${vkcv_includes} ${vkcv_camera_include} ${vkcv_gui_include} ${vkcv_shader_compiler_include})
+
+# linking with libraries from all dependencies and the VkCV framework
+target_link_libraries(wobble_bobble vkcv vkcv_camera vkcv_gui vkcv_shader_compiler)
diff --git a/projects/wobble_bobble/shaders/.gitignore b/projects/wobble_bobble/shaders/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/projects/wobble_bobble/shaders/grid.frag b/projects/wobble_bobble/shaders/grid.frag
new file mode 100644
index 0000000000000000000000000000000000000000..8eb2fbc2173bbddb5bdc44c52ce81d2e5d52d389
--- /dev/null
+++ b/projects/wobble_bobble/shaders/grid.frag
@@ -0,0 +1,30 @@
+#version 450
+
+layout(location = 0) in vec2 passPos;
+layout(location = 1) in vec3 passVelocity;
+layout(location = 2) in float passMass;
+
+layout(location = 0) out vec3 outColor;
+
+void main()	{
+    if (passMass <= 0.0f) {
+        discard;
+    }
+
+    const float value = length(passPos);
+
+    float z = sqrt(0.25 - value * value);
+
+    if (value < 0.5f) {
+        vec3 surface = vec3(passPos.x + 0.5f, passPos.y + 0.5f, z * 2.0f);
+        vec3 velocity = vec3(0.5f);
+
+        if (length(passVelocity) > 0.0f) {
+            velocity = vec3(0.5f) + 0.5f * normalize(passVelocity.xyz);
+        }
+
+        outColor = velocity;
+    } else {
+        discard;
+    }
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/grid.vert b/projects/wobble_bobble/shaders/grid.vert
new file mode 100644
index 0000000000000000000000000000000000000000..54de3f3e1da43e3ea3d018e5c54003b83e055c4c
--- /dev/null
+++ b/projects/wobble_bobble/shaders/grid.vert
@@ -0,0 +1,58 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+
+#include "particle.inc"
+
+layout(set=0, binding=0) uniform texture3D gridImage;
+layout(set=0, binding=1) uniform sampler gridSampler;
+
+layout(set=0, binding=2) uniform simulationBlock {
+    Simulation simulation;
+};
+
+layout(location = 0) in vec2 vertexPos;
+
+layout(location = 0) out vec2 passPos;
+layout(location = 1) out vec3 passVelocity;
+layout(location = 2) out float passMass;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+};
+
+ivec3 actual_mod(ivec3 x, ivec3 y) {
+    return x - y * (x/y);
+}
+
+void main()	{
+    ivec3 gridResolution = textureSize(sampler3D(gridImage, gridSampler), 0);
+
+    ivec3 gridID = ivec3(
+        gl_InstanceIndex,
+        gl_InstanceIndex / gridResolution.x,
+        gl_InstanceIndex / gridResolution.x / gridResolution.y
+    );
+
+    gridID = actual_mod(gridID, gridResolution);
+
+    vec3 position = (vec3(gridID) + vec3(0.5f)) / gridResolution;
+
+    vec3 size = vec3(1.0f) / vec3(gridResolution);
+    float volume = size.x * size.y * size.z;
+    float radius = cube_radius(volume);
+
+    vec4 gridData = texture(sampler3D(gridImage, gridSampler), position);
+
+    float mass = gridData.w;
+    float density = mass / volume;
+
+    float alpha = clamp(density / simulation.density, 0.0f, 1.0f);
+
+    passPos = vertexPos;
+    passVelocity = gridData.xyz;
+    passMass = mass;
+
+    // align voxel to face camera
+    gl_Position = mvp * vec4(position, 1);      // transform position into projected view space
+    gl_Position.xy += vertexPos * (radius * 2.0f) * alpha;  // move position directly in view space
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/init_particle_weights.comp b/projects/wobble_bobble/shaders/init_particle_weights.comp
new file mode 100644
index 0000000000000000000000000000000000000000..9b821e88fc7cc3fcea87b7eb6de0f8f6d629d911
--- /dev/null
+++ b/projects/wobble_bobble/shaders/init_particle_weights.comp
@@ -0,0 +1,43 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+
+layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in;
+
+#include "particle.inc"
+
+layout(set=0, binding=0, std430) restrict buffer particleBuffer {
+    Particle particles [];
+};
+
+layout(set=1, binding=0) uniform texture3D gridImage;
+layout(set=1, binding=1) uniform sampler gridSampler;
+
+void main()	{
+    if (gl_GlobalInvocationID.x < particles.length()) {
+        ParticleMinimal minimal = particles[gl_GlobalInvocationID.x].minimal;
+
+        minimal.weight_sum = 1.0f;
+
+        ivec3 gridResolution = textureSize(sampler3D(gridImage, gridSampler), 0);
+        ivec3 gridWindow = ivec3(minimal.size * 2.0f * gridResolution);
+
+        float weight_sum = 0.0f;
+
+        int i, j, k;
+
+        for (i = -gridWindow.x; i <= gridWindow.x; i++) {
+            for (j = -gridWindow.y; j <= gridWindow.y; j++) {
+                for (k = -gridWindow.z; k <= gridWindow.z; k++) {
+                    vec3 offset = vec3(i, j, k) / gridResolution;
+                    vec3 voxel = minimal.position + offset;
+
+                    weight_sum += voxel_particle_weight(voxel, minimal);
+                }
+            }
+        }
+
+        if (weight_sum > 0.0f) {
+            particles[gl_GlobalInvocationID.x].minimal.weight_sum = weight_sum;
+        }
+    }
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/lines.frag b/projects/wobble_bobble/shaders/lines.frag
new file mode 100644
index 0000000000000000000000000000000000000000..37d79d30d2ce751a59b1b467764b2fe464aa5c17
--- /dev/null
+++ b/projects/wobble_bobble/shaders/lines.frag
@@ -0,0 +1,7 @@
+#version 450
+
+layout(location = 0) out vec3 outColor;
+
+void main() {
+    outColor = vec3(1.0f);
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/lines.vert b/projects/wobble_bobble/shaders/lines.vert
new file mode 100644
index 0000000000000000000000000000000000000000..b8e3b01c67986156ad980899697ffea05409b752
--- /dev/null
+++ b/projects/wobble_bobble/shaders/lines.vert
@@ -0,0 +1,11 @@
+#version 450
+
+layout(location = 0) in vec3 vertexPos;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+};
+
+void main() {
+    gl_Position = mvp * vec4(vertexPos, 1);
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/particle.frag b/projects/wobble_bobble/shaders/particle.frag
new file mode 100644
index 0000000000000000000000000000000000000000..81c0a3594359e816a28b5e3f0301b47ce74a3cd7
--- /dev/null
+++ b/projects/wobble_bobble/shaders/particle.frag
@@ -0,0 +1,18 @@
+#version 450
+
+layout(location = 0) in vec2 passPos;
+layout(location = 1) in float passMass;
+
+layout(location = 0) out vec3 outColor;
+
+void main()	{
+    const float value = length(passPos);
+
+    float z = sqrt(0.25 - value * value);
+
+    if (value < 0.5f) {
+        outColor = vec3(passPos.x + 0.5f, passPos.y + 0.5f, z * 2.0f);
+    } else {
+        discard;
+    }
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/particle.inc b/projects/wobble_bobble/shaders/particle.inc
new file mode 100644
index 0000000000000000000000000000000000000000..a622485eab5bbcafdc51c030281e53b1cc1c5f11
--- /dev/null
+++ b/projects/wobble_bobble/shaders/particle.inc
@@ -0,0 +1,118 @@
+#ifndef PARTICLE_INC
+#define PARTICLE_INC
+
+#define EPSILON 0.00000001f
+
+struct ParticleMinimal {
+    vec3 position;
+    float size;
+    vec3 velocity;
+    float mass;
+	
+	vec3 pad;
+	float weight_sum;
+};
+
+struct Particle {
+    ParticleMinimal minimal;
+    mat4 deformation;
+	mat4 mls;
+};
+
+#define SIM_FORM_SPHERE 0
+#define SIM_FORM_CUBE 1
+
+#define SIM_TYPE_HYPERELASTIC 0
+#define SIM_TYPE_FLUID 1
+
+#define SIM_MODE_RANDOM 0
+#define SIM_MODE_ORDERED 1
+
+struct Simulation {
+	float density;
+	float size;
+	float lame1;
+	float lame2;
+	
+	int form;
+	int type;
+	float K;
+	float E;
+	
+	float gamma;
+	int mode;
+	float gravity;
+	int count;
+};
+
+const float PI = 3.1415926535897932384626433832795;
+
+float sphere_volume(float radius) {
+	return 4.0f * (radius * radius * radius) * PI / 3.0f;
+}
+
+float sphere_radius(float volume) {
+	return pow(volume * 3.0f / 4.0f / PI, 1.0f / 3.0f);
+}
+
+float cube_volume(float radius) {
+	return 8.0f * (radius * radius * radius);
+}
+
+float cube_radius(float volume) {
+	return pow(volume / 8.0f, 1.0f / 3.0f);
+}
+
+float weight_A(float x) {
+	return max(1.0f - x, 0.0f);
+}
+
+float weight_B(float x) {
+	if (x < 0.5f) {
+		return 0.75f - x * x;
+	} else
+	if (x < 1.5f) {
+		float y = (1.5f - x);
+		return 0.5f * y * y;
+	} else {
+		return 0.0f;
+	}
+}
+
+float weight_C(float x) {
+	if (x < 1.0f) {
+		return (0.5f * x - 1.0f) * x*x + 2.0f / 3.0f;
+	} else
+	if (x < 2.0f) {
+		float y = (2.0f - x);
+		return 0.5f / 3.0f * y * y * y;
+	} else {
+		return 0.0f;
+	}
+}
+
+float voxel_particle_weight(vec3 voxel, ParticleMinimal particle) {
+	vec3 delta = abs(particle.position - voxel) / particle.size;
+	
+	if (any(isnan(delta)) || any(isinf(delta))) {
+		return 0.0f;
+	}
+	
+	vec3 weight = vec3(
+		weight_C(delta.x),
+		weight_C(delta.y),
+		weight_C(delta.z)
+	);
+	
+	float result = (
+		weight.x * weight.y * weight.z
+	) / particle.weight_sum;
+	
+	if ((isnan(result)) || (isinf(result))) {
+		return 0.0f;
+	} else {
+		return result;
+	}
+}
+
+#endif // PARTICLE_INC
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/particle.vert b/projects/wobble_bobble/shaders/particle.vert
new file mode 100644
index 0000000000000000000000000000000000000000..a8f697e79eacba361d80b5858405add675e761bb
--- /dev/null
+++ b/projects/wobble_bobble/shaders/particle.vert
@@ -0,0 +1,31 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+
+#include "particle.inc"
+
+layout(set=0, binding=0, std430) readonly buffer particleBuffer {
+    Particle particles [];
+};
+
+layout(location = 0) in vec2 vertexPos;
+
+layout(location = 0) out vec2 passPos;
+layout(location = 1) out float passMass;
+
+layout( push_constant ) uniform constants{
+    mat4 mvp;
+};
+
+void main()	{
+    vec3 position = particles[gl_InstanceIndex].minimal.position;
+    float size = particles[gl_InstanceIndex].minimal.size;
+
+    float mass = particles[gl_InstanceIndex].minimal.mass;
+
+    passPos = vertexPos;
+    passMass = mass;
+
+    // align particle to face camera
+    gl_Position = mvp * vec4(position, 1);      // transform position into projected view space
+    gl_Position.xy += vertexPos * size * 2.0f;  // move position directly in view space
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/transform_particles_to_grid.comp b/projects/wobble_bobble/shaders/transform_particles_to_grid.comp
new file mode 100644
index 0000000000000000000000000000000000000000..1be18c41303ab2208c8cb4a8c33f41b350315d63
--- /dev/null
+++ b/projects/wobble_bobble/shaders/transform_particles_to_grid.comp
@@ -0,0 +1,101 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+
+layout(local_size_x = 4, local_size_y = 4, local_size_z = 4) in;
+
+#include "particle.inc"
+
+layout(set=0, binding=0, std430) readonly buffer particleBuffer {
+    Particle particles [];
+};
+
+layout(set=1, binding=0) uniform simulationBlock {
+    Simulation simulation;
+};
+
+layout(set=2, binding=0, rgba16f) restrict writeonly uniform image3D gridImage;
+
+layout( push_constant ) uniform constants {
+    float t;
+    float dt;
+    float speedfactor;
+};
+
+#define SHARED_PARTICLES_BATCH_SIZE 64
+
+shared ParticleMinimal shared_particles [SHARED_PARTICLES_BATCH_SIZE];
+
+void main()	{
+    const vec3 position = (vec3(gl_GlobalInvocationID) + vec3(0.5f)) / imageSize(gridImage);
+
+    float dts = dt * speedfactor;
+
+    vec4 gridValue = vec4(0.0f);
+
+    uint offset = 0;
+
+    for (offset = 0; offset < particles.length(); offset += SHARED_PARTICLES_BATCH_SIZE) {
+        uint localOffset = offset + gl_LocalInvocationIndex;
+
+        if (localOffset < particles.length()) {
+            shared_particles[gl_LocalInvocationIndex] = particles[localOffset].minimal;
+
+            shared_particles[gl_LocalInvocationIndex].pad = (
+                mat3(particles[localOffset].mls) *
+                (position - shared_particles[gl_LocalInvocationIndex].position)
+            ) + (
+                shared_particles[gl_LocalInvocationIndex].velocity *
+                shared_particles[gl_LocalInvocationIndex].mass
+            );
+        } else {
+            shared_particles[gl_LocalInvocationIndex].position = vec3(0.0f);
+            shared_particles[gl_LocalInvocationIndex].size = 0.0f;
+            shared_particles[gl_LocalInvocationIndex].velocity = vec3(0.0f);
+            shared_particles[gl_LocalInvocationIndex].mass = 0.0f;
+
+            shared_particles[gl_LocalInvocationIndex].pad = vec3(0.0f);
+            shared_particles[gl_LocalInvocationIndex].weight_sum = 1.0f;
+        }
+
+        barrier();
+        memoryBarrierShared();
+
+        for (uint i = 0; i < SHARED_PARTICLES_BATCH_SIZE; i++) {
+            float weight = voxel_particle_weight(position, shared_particles[i]);
+
+            gridValue += vec4(
+                shared_particles[i].pad * weight,
+                shared_particles[i].mass * weight
+            );
+        }
+
+        barrier();
+        memoryBarrierShared();
+    }
+
+    if (any(isnan(gridValue.xyz)) || any(isinf(gridValue.xyz))) {
+        gridValue.xyz = vec3(0.0f);
+    }
+
+    gridValue.xyz += vec3(0.0f, -simulation.gravity * dts * gridValue.w, 0.0f);
+
+    bvec3 lowerID = lessThanEqual(gl_GlobalInvocationID, ivec3(0));
+    bvec3 negativeVelocity = lessThan(gridValue.xyz, vec3(0.0f));
+
+    bvec3 greaterID = greaterThanEqual(gl_GlobalInvocationID + ivec3(1), imageSize(gridImage));
+    bvec3 positiveVelocity = greaterThan(gridValue.xyz, vec3(0.0f));
+
+    bvec3 collision = bvec3(
+        (lowerID.x && negativeVelocity.x) || (greaterID.x && positiveVelocity.x),
+        (lowerID.y && negativeVelocity.y) || (greaterID.y && positiveVelocity.y),
+        (lowerID.z && negativeVelocity.z) || (greaterID.z && positiveVelocity.z)
+    );
+
+    gridValue.xyz = mix(gridValue.xyz, -gridValue.xyz, collision);
+
+    imageStore(
+        gridImage,
+        ivec3(gl_GlobalInvocationID),
+        gridValue
+    );
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/shaders/update_particle_velocities.comp b/projects/wobble_bobble/shaders/update_particle_velocities.comp
new file mode 100644
index 0000000000000000000000000000000000000000..4420bc9575bcde4d04d7d4f5531d0091362285c2
--- /dev/null
+++ b/projects/wobble_bobble/shaders/update_particle_velocities.comp
@@ -0,0 +1,161 @@
+#version 450
+#extension GL_GOOGLE_include_directive : enable
+#extension GL_EXT_control_flow_attributes : enable
+
+layout(local_size_x = 64, local_size_y = 1, local_size_z = 1) in;
+
+#include "particle.inc"
+
+layout(set=0, binding=0, std430) restrict buffer particleBuffer {
+    Particle particles [];
+};
+
+layout(set=1, binding=0) uniform simulationBlock {
+    Simulation simulation;
+};
+
+layout(set=2, binding=0) uniform texture3D gridImage;
+layout(set=2, binding=1) uniform sampler gridSampler;
+
+layout( push_constant ) uniform constants {
+    float t;
+    float dt;
+    float speedfactor;
+};
+
+void main()	{
+    float dts = dt * speedfactor;
+
+    if (gl_GlobalInvocationID.x < particles.length()) {
+        Particle particle = particles[gl_GlobalInvocationID.x];
+
+        vec3 position = particle.minimal.position;
+        float size = particle.minimal.size;
+        float mass = particle.minimal.mass;
+
+        ivec3 gridResolution = textureSize(sampler3D(gridImage, gridSampler), 0);
+        ivec3 gridWindow = ivec3(size * 2.0f * gridResolution);
+
+        mat3 affine_D = mat3(0.0f);
+        mat3 affine_B = mat3(0.0f);
+
+        vec3 velocity_pic = vec3(0.0f);
+        vec3 velocity_flip = vec3(particle.minimal.velocity);
+
+        int i, j, k;
+
+        for (i = -gridWindow.x; i <= gridWindow.x; i++) {
+            for (j = -gridWindow.y; j <= gridWindow.y; j++) {
+                for (k = -gridWindow.z; k <= gridWindow.z; k++) {
+                    vec3 offset = vec3(i, j, k) / gridResolution;
+                    vec3 voxel = position + offset;
+
+                    vec4 gridSample = texture(sampler3D(gridImage, gridSampler), voxel);
+
+                    float weight = voxel_particle_weight(voxel, particle.minimal);
+                    vec3 velocity = gridSample.xyz * weight / gridSample.w;
+
+                    if (any(isnan(velocity)) || any(isinf(velocity))) {
+                        velocity = vec3(0.0f);
+                    }
+
+                    affine_D += outerProduct(weight * offset, offset);
+                    affine_B += outerProduct(velocity, offset);
+
+                    velocity_pic += velocity;
+                }
+            }
+        }
+
+        mat3 mls_Q = mat3(0.0f);
+        mat3 affine_C = mat3(0.0f);
+
+        mat3 F = mat3(particle.deformation);
+
+        mat3 D_inv = inverse(affine_D);
+        float D_det = determinant(D_inv);
+
+        if ((isnan(D_det)) || (isinf(D_det))) {
+            D_inv = mat3(0.0f);
+        } else {
+            D_inv *= min(abs(D_det), 1.0f / EPSILON) / abs(D_det);
+        }
+
+        float J = max(determinant(F), EPSILON);
+        float volume = sphere_volume(size);
+
+        mat3 stress = mat3(0.0f);
+
+        switch (simulation.type) {
+            case SIM_TYPE_HYPERELASTIC:
+                mat3 F_T = transpose(F);
+                mat3 F_T_inv = inverse(F_T);
+
+                mat3 P_term_0 = simulation.lame2 * (F - F_T_inv);
+                mat3 P_term_1 = simulation.lame1 * log(J) * F_T_inv;
+
+                mat3 P = P_term_0 + P_term_1;
+
+                stress = P * F_T;
+                break;
+            case SIM_TYPE_FLUID:
+                float pressure = simulation.K * (1.0f / pow(J, simulation.gamma) - 1.0f);
+
+                stress = mat3(-pressure * J);
+                break;
+            default:
+                break;
+        }
+
+        mls_Q -= dts * volume * stress * D_inv;
+
+        affine_C = affine_B * D_inv;
+        mls_Q += affine_C * mass;
+
+        F = (mat3(1.0f) + dts * affine_C) * F;
+
+        position = position + velocity_pic * dts;
+
+        const float gridRange = (1.0f - 2.0f * size);
+
+        for (uint i = 0; i < 3; i++) {
+            if (position[i] - size < 0.0f) {
+                float a = (size - position[i]) / gridRange;
+                int b = int(floor(a));
+
+                a = (a - b) * gridRange;
+
+                if (b % 2 == 0) {
+                    position[i] = size + a;
+                } else {
+                    position[i] = 1.0f - size - a;
+                }
+
+                if ((velocity_pic[i] < 0.0f) == (b % 2 == 0)) {
+                    velocity_pic[i] *= -1.0f;
+                }
+            } else
+            if (position[i] + size > 1.0f) {
+                float a = (position[i] + size - 1.0f) / gridRange;
+                int b = int(floor(a));
+
+                a = (a - b) * gridRange;
+
+                if (b % 2 == 0) {
+                    position[i] = 1.0f - size - a;
+                } else {
+                    position[i] = size + a;
+                }
+
+                if ((velocity_pic[i] > 0.0f) == (b % 2 == 0)) {
+                    velocity_pic[i] *= -1.0f;
+                }
+            }
+        }
+
+        particles[gl_GlobalInvocationID.x].minimal.position = position;
+        particles[gl_GlobalInvocationID.x].minimal.velocity = velocity_pic;
+        particles[gl_GlobalInvocationID.x].deformation = mat4(F);
+        particles[gl_GlobalInvocationID.x].mls = mat4(mls_Q);
+    }
+}
\ No newline at end of file
diff --git a/projects/wobble_bobble/src/main.cpp b/projects/wobble_bobble/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..96c367f2f8e22ac65bf6ccf6a5c09eb238b5604d
--- /dev/null
+++ b/projects/wobble_bobble/src/main.cpp
@@ -0,0 +1,905 @@
+
+#include <vkcv/Core.hpp>
+#include <vkcv/camera/CameraManager.hpp>
+#include <vkcv/gui/GUI.hpp>
+#include <vkcv/shader/GLSLCompiler.hpp>
+
+#include <random>
+
+struct Particle {
+	glm::vec3 position;
+	float size;
+	glm::vec3 velocity;
+	float mass;
+	
+	glm::vec3 pad;
+	float weight_sum;
+	
+	glm::mat4 deformation;
+	glm::mat4 mls;
+};
+
+#define SIM_FORM_SPHERE 0
+#define SIM_FORM_CUBE 1
+
+#define SIM_TYPE_HYPERELASTIC 0
+#define SIM_TYPE_FLUID 1
+
+#define SIM_MODE_RANDOM 0
+#define SIM_MODE_ORDERED 1
+
+struct Simulation {
+	float density;
+	float size;
+	float lame1;
+	float lame2;
+	
+	int form;
+	int type;
+	float K;
+	float E;
+	
+	float gamma;
+	int mode;
+	float gravity;
+	int count;
+};
+
+struct Physics {
+	float t;
+	float dt;
+	float speedfactor;
+};
+
+float sphere_volume(float radius) {
+	return 4.0f * (radius * radius * radius) * M_PI / 3.0f;
+}
+
+float sphere_radius(float volume) {
+	return std::pow(volume * 3.0f / 4.0f / M_PI, 1.0f / 3.0f);
+}
+
+float cube_volume(float radius) {
+	return 8.0f * (radius * radius * radius);
+}
+
+float cube_radius(float volume) {
+	return std::pow(volume / 8.0f, 1.0f / 3.0f);
+}
+
+std::random_device random_dev;
+std::uniform_int_distribution<int> dist(0, RAND_MAX);
+
+float randomFloat(float min, float max) {
+	return min + (max - min) * dist(random_dev) / static_cast<float>(RAND_MAX);
+}
+
+float mod(float x, float y) {
+	return x - std::floor(x / y) * y;
+}
+
+void distributeParticlesCube(Particle *particles, size_t count, const glm::vec3& center, float radius,
+							 float mass, const glm::vec3& velocity, bool random) {
+	const float side = cube_radius(static_cast<float>(count)) * 2.0f;
+	
+	float volume = 0.0f;
+	
+	for (size_t i = 0; i < count; i++) {
+		glm::vec3 offset;
+		
+		if (random) {
+			offset.x = randomFloat(-1.0f, +1.0f);
+			offset.y = randomFloat(-1.0f, +1.0f);
+			offset.z = randomFloat(-1.0f, +1.0f);
+		} else {
+			const float s = static_cast<float>(i) + 0.5f;
+			
+			offset.x = 2.0f * mod(s, side) / side - 1.0f;
+			offset.y = 2.0f * mod(s / side, side) / side - 1.0f;
+			offset.z = 2.0f * mod(s / side / side, side) / side - 1.0f;
+		}
+		
+		offset *= radius;
+		
+		float size = 0.0f;
+		
+		if (random) {
+			const float ax = std::abs(offset.x);
+			const float ay = std::abs(offset.y);
+			const float az = std::abs(offset.z);
+			
+			const float a = std::max(std::max(ax, ay), az);
+			
+			size = (radius - a);
+		} else {
+			size = 2.0f * radius / side;
+		}
+		
+		particles[i].position = center + offset;
+		particles[i].size = size;
+		particles[i].velocity = velocity;
+		
+		volume += cube_volume(size);
+	}
+	
+	for (size_t i = 0; i < count; i++) {
+		particles[i].mass = (mass * cube_volume(particles[i].size) / volume);
+		particles[i].deformation = glm::mat4(1.0f);
+		
+		particles[i].pad = glm::vec3(0.0f);
+		particles[i].weight_sum = 1.0f;
+		
+		particles[i].mls = glm::mat4(0.0f);
+	}
+}
+
+void distributeParticlesSphere(Particle *particles, size_t count, const glm::vec3& center, float radius,
+							   float mass, const glm::vec3& velocity, bool random) {
+	const float side = sphere_radius(static_cast<float>(count)) * 2.0f;
+	
+	float volume = 0.0f;
+	
+	for (size_t i = 0; i < count; i++) {
+		glm::vec3 offset;
+		
+		if (random) {
+			offset.x = randomFloat(-1.0f, +1.0f);
+			offset.y = randomFloat(-1.0f, +1.0f);
+			offset.z = randomFloat(-1.0f, +1.0f);
+			
+			if (glm::length(offset) > 0.0f)
+				offset = glm::normalize(offset);
+			
+			offset *= randomFloat(0.0f, 1.0f);
+		} else {
+			const float range = 0.5f * side;
+			const float s = static_cast<float>(i) + 0.5f;
+			
+			float a = mod(s, range) / range;
+			float b = mod(s / range, M_PI * range);
+			float c = mod(s / range / M_PI / range, M_PI * range * 2.0f);
+			
+			offset.x = a * std::sin(c) * std::sin(b);
+			offset.y = a * std::cos(b);
+			offset.z = a * std::cos(c) * std::sin(b);
+		}
+		
+		offset *= radius;
+		
+		float size = 0.0f;
+		
+		if (random) {
+			size = (radius - glm::length(offset));
+		} else {
+			size = 2.0f * radius / side;
+		}
+		
+		particles[i].position = center + offset;
+		particles[i].size = size;
+		particles[i].velocity = velocity;
+		
+		volume += sphere_volume(size);
+	}
+	
+	// Keep the same densitiy as planned!
+	mass *= (volume / sphere_volume(radius));
+	
+	for (size_t i = 0; i < count; i++) {
+		particles[i].mass = (mass * sphere_volume(particles[i].size) / volume);
+		particles[i].deformation = glm::mat4(1.0f);
+		
+		particles[i].pad = glm::vec3(0.0f);
+		particles[i].weight_sum = 1.0f;
+		
+		particles[i].mls = glm::mat4(0.0f);
+	}
+}
+
+vkcv::ComputePipelineHandle createComputePipeline(vkcv::Core& core, vkcv::shader::GLSLCompiler& compiler,
+												  const std::string& path,
+												  std::vector<vkcv::DescriptorSetHandle>& descriptorSets) {
+	vkcv::ShaderProgram shaderProgram;
+	
+	compiler.compile(
+			vkcv::ShaderStage::COMPUTE,
+			path,
+			[&shaderProgram](vkcv::ShaderStage stage, const std::filesystem::path& path) {
+				shaderProgram.addShader(stage, path);
+			}
+	);
+	
+	const auto& descriptors = shaderProgram.getReflectedDescriptors();
+	
+	size_t count = 0;
+	
+	for (const auto& descriptor : descriptors) {
+		if (descriptor.first >= count) {
+			count = (descriptor.first + 1);
+		}
+	}
+	
+	std::vector<vkcv::DescriptorSetLayoutHandle> descriptorSetLayouts;
+	
+	descriptorSetLayouts.resize(count);
+	descriptorSets.resize(count);
+	
+	for (const auto& descriptor : descriptors) {
+		descriptorSetLayouts[descriptor.first] = core.createDescriptorSetLayout(descriptor.second);
+		descriptorSets[descriptor.first] = core.createDescriptorSet(descriptorSetLayouts[descriptor.first]);
+	}
+	
+	vkcv::ComputePipelineConfig config {
+			shaderProgram,
+			descriptorSetLayouts
+	};
+	
+	return core.createComputePipeline(config);
+}
+
+vkcv::BufferHandle resetParticles(vkcv::Core& core, size_t count, const glm::vec3& velocity,
+					float density, float size, int form, int mode) {
+	vkcv::Buffer<Particle> particles = core.createBuffer<Particle>(
+			vkcv::BufferType::STORAGE,
+			count
+	);
+	
+	std::vector<Particle> particles_vec (particles.getCount());
+	
+	switch (form) {
+		case SIM_FORM_SPHERE:
+			distributeParticlesSphere(
+					particles_vec.data(),
+					particles_vec.size(),
+					glm::vec3(0.5f),
+					size,
+					density * sphere_volume(size),
+					velocity,
+					(mode == 0)
+			);
+			break;
+		case SIM_FORM_CUBE:
+			distributeParticlesCube(
+					particles_vec.data(),
+					particles_vec.size(),
+					glm::vec3(0.5f),
+					size,
+					density * sphere_volume(size),
+					velocity,
+					(mode == 0)
+			);
+			break;
+		default:
+			break;
+	}
+	
+	particles.fill(particles_vec);
+	return particles.getHandle();
+}
+
+int main(int argc, const char **argv) {
+	const char* applicationName = "Wobble Bobble";
+	
+	uint32_t windowWidth = 800;
+	uint32_t windowHeight = 600;
+	
+	vkcv::Features features;
+	features.requireExtension(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
+	
+	vkcv::Core core = vkcv::Core::create(
+			applicationName,
+			VK_MAKE_VERSION(0, 0, 1),
+			{vk::QueueFlagBits::eTransfer, vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute},
+			features
+	);
+	vkcv::WindowHandle windowHandle = core.createWindow(applicationName, windowWidth, windowHeight, true);
+	vkcv::Window& window = core.getWindow(windowHandle);
+	vkcv::camera::CameraManager cameraManager(window);
+	
+	vkcv::gui::GUI gui (core, windowHandle);
+	
+	uint32_t trackballIdx = cameraManager.addCamera(vkcv::camera::ControllerType::TRACKBALL);
+	cameraManager.getCamera(trackballIdx).setCenter(glm::vec3(0.5f, 0.5f, 0.5f));   // set camera to look at the center of the particle volume
+	cameraManager.addCamera(vkcv::camera::ControllerType::PILOT);
+	
+	auto swapchainExtent = core.getSwapchain(windowHandle).getExtent();
+	
+	vkcv::ImageHandle depthBuffer = core.createImage(
+			vk::Format::eD32Sfloat,
+			swapchainExtent.width,
+			swapchainExtent.height
+	).getHandle();
+	
+	vkcv::Image grid = core.createImage(
+			vk::Format::eR16G16B16A16Sfloat,
+			32,
+			32,
+			32,
+			false,
+			true
+	);
+	
+	vkcv::SamplerHandle gridSampler = core.createSampler(
+			vkcv::SamplerFilterType::LINEAR,
+			vkcv::SamplerFilterType::LINEAR,
+			vkcv::SamplerMipmapMode::NEAREST,
+			vkcv::SamplerAddressMode::CLAMP_TO_BORDER,
+			0.0f,
+			vkcv::SamplerBorderColor::FLOAT_ZERO_TRANSPARENT
+	);
+	
+	vkcv::Buffer<Simulation> simulation = core.createBuffer<Simulation>(
+			vkcv::BufferType::UNIFORM, 1, vkcv::BufferMemoryType::HOST_VISIBLE
+	);
+	
+	Simulation* sim = simulation.map();
+	
+	glm::vec3 initialVelocity (0.0f, 0.1f, 0.0f);
+	
+	sim->density = 2500.0f;
+	sim->size = 0.1f;
+	sim->lame1 = 10.0f;
+	sim->lame2 = 20.0f;
+	sim->form = SIM_FORM_SPHERE;
+	sim->type = SIM_TYPE_HYPERELASTIC;
+	sim->K = 2.2f;
+	sim->E = 35.0f;
+	sim->gamma = 1.330f;
+	sim->mode = SIM_MODE_RANDOM;
+	sim->gravity = 9.81f;
+	sim->count = 1024;
+	
+	vkcv::BufferHandle particlesHandle = resetParticles(
+			core,
+			sim->count,
+			initialVelocity,
+			sim->density,
+			sim->size,
+			sim->form,
+			sim->mode
+	);
+	
+	vkcv::shader::GLSLCompiler compiler;
+	
+	std::vector<vkcv::DescriptorSetHandle> initParticleWeightsSets;
+	vkcv::ComputePipelineHandle initParticleWeightsPipeline = createComputePipeline(
+			core, compiler,
+			"shaders/init_particle_weights.comp",
+			initParticleWeightsSets
+	);
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.storageBufferWrites.push_back(vkcv::BufferDescriptorWrite(0, particlesHandle));
+		core.writeDescriptorSet(initParticleWeightsSets[0], writes);
+	}
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.sampledImageWrites.push_back(vkcv::SampledImageDescriptorWrite(0, grid.getHandle()));
+		writes.samplerWrites.push_back(vkcv::SamplerDescriptorWrite(1, gridSampler));
+		core.writeDescriptorSet(initParticleWeightsSets[1], writes);
+	}
+	
+	std::vector<vkcv::DescriptorSetHandle> transformParticlesToGridSets;
+	vkcv::ComputePipelineHandle transformParticlesToGridPipeline = createComputePipeline(
+			core, compiler,
+			"shaders/transform_particles_to_grid.comp",
+			transformParticlesToGridSets
+	);
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.storageBufferWrites.push_back(vkcv::BufferDescriptorWrite(0, particlesHandle));
+		core.writeDescriptorSet(transformParticlesToGridSets[0], writes);
+	}
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.uniformBufferWrites.push_back(vkcv::BufferDescriptorWrite(0, simulation.getHandle()));
+		core.writeDescriptorSet(transformParticlesToGridSets[1], writes);
+	}
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.storageImageWrites.push_back(vkcv::StorageImageDescriptorWrite(0, grid.getHandle()));
+		core.writeDescriptorSet(transformParticlesToGridSets[2], writes);
+	}
+	
+	std::vector<vkcv::DescriptorSetHandle> updateParticleVelocitiesSets;
+	vkcv::ComputePipelineHandle updateParticleVelocitiesPipeline = createComputePipeline(
+			core, compiler,
+			"shaders/update_particle_velocities.comp",
+			updateParticleVelocitiesSets
+	);
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.storageBufferWrites.push_back(vkcv::BufferDescriptorWrite(0, particlesHandle));
+		core.writeDescriptorSet(updateParticleVelocitiesSets[0], writes);
+	}
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.uniformBufferWrites.push_back(vkcv::BufferDescriptorWrite(0, simulation.getHandle()));
+		core.writeDescriptorSet(updateParticleVelocitiesSets[1], writes);
+	}
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.sampledImageWrites.push_back(vkcv::SampledImageDescriptorWrite(0, grid.getHandle()));
+		writes.samplerWrites.push_back(vkcv::SamplerDescriptorWrite(1, gridSampler));
+		core.writeDescriptorSet(updateParticleVelocitiesSets[2], writes);
+	}
+	
+	vkcv::ShaderProgram gfxProgramGrid;
+	
+	compiler.compileProgram(gfxProgramGrid, {
+			{ vkcv::ShaderStage::VERTEX, "shaders/grid.vert" },
+			{ vkcv::ShaderStage::FRAGMENT, "shaders/grid.frag" }
+	}, nullptr);
+	
+	vkcv::ShaderProgram gfxProgramParticles;
+	
+	compiler.compileProgram(gfxProgramParticles, {
+		{ vkcv::ShaderStage::VERTEX, "shaders/particle.vert" },
+		{ vkcv::ShaderStage::FRAGMENT, "shaders/particle.frag" }
+	}, nullptr);
+	
+	vkcv::ShaderProgram gfxProgramLines;
+	
+	compiler.compileProgram(gfxProgramLines, {
+			{ vkcv::ShaderStage::VERTEX, "shaders/lines.vert" },
+			{ vkcv::ShaderStage::FRAGMENT, "shaders/lines.frag" }
+	}, nullptr);
+	
+	vkcv::PassConfig passConfigGrid ({
+		vkcv::AttachmentDescription(
+				vkcv::AttachmentOperation::STORE,
+				vkcv::AttachmentOperation::CLEAR,
+				core.getSwapchain(windowHandle).getFormat()
+		),
+		vkcv::AttachmentDescription(
+				vkcv::AttachmentOperation::STORE,
+				vkcv::AttachmentOperation::CLEAR,
+				vk::Format::eD32Sfloat
+		)
+	});
+	
+	vkcv::PassConfig passConfigParticles ({
+		vkcv::AttachmentDescription(
+				vkcv::AttachmentOperation::STORE,
+				vkcv::AttachmentOperation::CLEAR,
+				core.getSwapchain(windowHandle).getFormat()
+		),
+		vkcv::AttachmentDescription(
+				vkcv::AttachmentOperation::STORE,
+				vkcv::AttachmentOperation::CLEAR,
+				vk::Format::eD32Sfloat
+		)
+	});
+	
+	vkcv::PassConfig passConfigLines ({
+		vkcv::AttachmentDescription(
+				vkcv::AttachmentOperation::STORE,
+				vkcv::AttachmentOperation::LOAD,
+				core.getSwapchain(windowHandle).getFormat()
+		),
+		vkcv::AttachmentDescription(
+				vkcv::AttachmentOperation::STORE,
+				vkcv::AttachmentOperation::LOAD,
+				vk::Format::eD32Sfloat
+		)
+	});
+	
+	vkcv::DescriptorSetLayoutHandle gfxSetLayoutGrid = core.createDescriptorSetLayout(
+			gfxProgramGrid.getReflectedDescriptors().at(0)
+	);
+	
+	vkcv::DescriptorSetHandle gfxSetGrid = core.createDescriptorSet(gfxSetLayoutGrid);
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.sampledImageWrites.push_back(vkcv::SampledImageDescriptorWrite(0, grid.getHandle()));
+		writes.samplerWrites.push_back(vkcv::SamplerDescriptorWrite(1, gridSampler));
+		writes.uniformBufferWrites.push_back(vkcv::BufferDescriptorWrite(2, simulation.getHandle()));
+		core.writeDescriptorSet(gfxSetGrid, writes);
+	}
+	
+	vkcv::DescriptorSetLayoutHandle gfxSetLayoutParticles = core.createDescriptorSetLayout(
+			gfxProgramParticles.getReflectedDescriptors().at(0)
+	);
+	
+	vkcv::DescriptorSetHandle gfxSetParticles = core.createDescriptorSet(gfxSetLayoutParticles);
+	
+	{
+		vkcv::DescriptorWrites writes;
+		writes.storageBufferWrites.push_back(vkcv::BufferDescriptorWrite(0, particlesHandle));
+		core.writeDescriptorSet(gfxSetParticles, writes);
+	}
+	
+	vkcv::PassHandle gfxPassGrid = core.createPass(passConfigGrid);
+	vkcv::PassHandle gfxPassParticles = core.createPass(passConfigParticles);
+	vkcv::PassHandle gfxPassLines = core.createPass(passConfigLines);
+	
+	vkcv::VertexLayout vertexLayoutGrid ({
+		vkcv::VertexBinding(0, gfxProgramGrid.getVertexAttachments())
+	});
+	
+	vkcv::GraphicsPipelineConfig gfxPipelineConfigGrid;
+	gfxPipelineConfigGrid.m_ShaderProgram = gfxProgramGrid;
+	gfxPipelineConfigGrid.m_Width = windowWidth;
+	gfxPipelineConfigGrid.m_Height = windowHeight;
+	gfxPipelineConfigGrid.m_PassHandle = gfxPassGrid;
+	gfxPipelineConfigGrid.m_VertexLayout = vertexLayoutGrid;
+	gfxPipelineConfigGrid.m_DescriptorLayouts = { gfxSetLayoutGrid };
+	gfxPipelineConfigGrid.m_UseDynamicViewport = true;
+	
+	vkcv::VertexLayout vertexLayoutParticles ({
+		vkcv::VertexBinding(0, gfxProgramParticles.getVertexAttachments())
+	});
+	
+	vkcv::GraphicsPipelineConfig gfxPipelineConfigParticles;
+	gfxPipelineConfigParticles.m_ShaderProgram = gfxProgramParticles;
+	gfxPipelineConfigParticles.m_Width = windowWidth;
+	gfxPipelineConfigParticles.m_Height = windowHeight;
+	gfxPipelineConfigParticles.m_PassHandle = gfxPassParticles;
+	gfxPipelineConfigParticles.m_VertexLayout = vertexLayoutParticles;
+	gfxPipelineConfigParticles.m_DescriptorLayouts = { gfxSetLayoutParticles };
+	gfxPipelineConfigParticles.m_UseDynamicViewport = true;
+	
+	vkcv::VertexLayout vertexLayoutLines ({
+		vkcv::VertexBinding(0, gfxProgramLines.getVertexAttachments())
+	});
+	
+	vkcv::GraphicsPipelineConfig gfxPipelineConfigLines;
+	gfxPipelineConfigLines.m_ShaderProgram = gfxProgramLines;
+	gfxPipelineConfigLines.m_Width = windowWidth;
+	gfxPipelineConfigLines.m_Height = windowHeight;
+	gfxPipelineConfigLines.m_PassHandle = gfxPassLines;
+	gfxPipelineConfigLines.m_VertexLayout = vertexLayoutLines;
+	gfxPipelineConfigLines.m_DescriptorLayouts = {};
+	gfxPipelineConfigLines.m_UseDynamicViewport = true;
+	gfxPipelineConfigLines.m_PrimitiveTopology = vkcv::PrimitiveTopology::LineList;
+	
+	vkcv::GraphicsPipelineHandle gfxPipelineGrid = core.createGraphicsPipeline(gfxPipelineConfigGrid);
+	vkcv::GraphicsPipelineHandle gfxPipelineParticles = core.createGraphicsPipeline(gfxPipelineConfigParticles);
+	vkcv::GraphicsPipelineHandle gfxPipelineLines = core.createGraphicsPipeline(gfxPipelineConfigLines);
+	
+	vkcv::Buffer<glm::vec2> trianglePositions = core.createBuffer<glm::vec2>(vkcv::BufferType::VERTEX, 3);
+	trianglePositions.fill({
+		glm::vec2(-1.0f, -1.0f),
+		glm::vec2(+0.0f, +1.5f),
+		glm::vec2(+1.0f, -1.0f)
+	});
+	
+	vkcv::Buffer<uint16_t> triangleIndices = core.createBuffer<uint16_t>(vkcv::BufferType::INDEX, 3);
+	triangleIndices.fill({
+		0, 1, 2
+	});
+	
+	vkcv::Mesh triangleMesh (
+			{ vkcv::VertexBufferBinding(0, trianglePositions.getVulkanHandle()) },
+			triangleIndices.getVulkanHandle(),
+			triangleIndices.getCount()
+	);
+	
+	vkcv::Buffer<glm::vec3> linesPositions = core.createBuffer<glm::vec3>(vkcv::BufferType::VERTEX, 8);
+	linesPositions.fill({
+		glm::vec3(0.0f, 0.0f, 0.0f),
+		glm::vec3(1.0f, 0.0f, 0.0f),
+		glm::vec3(0.0f, 1.0f, 0.0f),
+		glm::vec3(1.0f, 1.0f, 0.0f),
+		glm::vec3(0.0f, 0.0f, 1.0f),
+		glm::vec3(1.0f, 0.0f, +1.0f),
+		glm::vec3(0.0f, 1.0f, 1.0f),
+		glm::vec3(1.0f, 1.0f, 1.0f)
+	});
+	
+	vkcv::Buffer<uint16_t> linesIndices = core.createBuffer<uint16_t>(vkcv::BufferType::INDEX, 24);
+	linesIndices.fill({
+		0, 1,
+		1, 3,
+		3, 2,
+		2, 0,
+		
+		4, 5,
+		5, 7,
+		7, 6,
+		6, 4,
+		
+		0, 4,
+		1, 5,
+		2, 6,
+		3, 7
+	});
+	
+	vkcv::Mesh linesMesh (
+			{ vkcv::VertexBufferBinding(0, linesPositions.getVulkanHandle()) },
+			linesIndices.getVulkanHandle(),
+			linesIndices.getCount()
+	);
+	
+	std::vector<vkcv::DrawcallInfo> drawcallsGrid;
+	
+	drawcallsGrid.push_back(vkcv::DrawcallInfo(
+			triangleMesh,
+			{ vkcv::DescriptorSetUsage(0, gfxSetGrid) },
+			grid.getWidth() * grid.getHeight() * grid.getDepth()
+	));
+	
+	std::vector<vkcv::DrawcallInfo> drawcallsParticles;
+	
+	drawcallsParticles.push_back(vkcv::DrawcallInfo(
+			triangleMesh,
+			{ vkcv::DescriptorSetUsage(0, gfxSetParticles) },
+			sim->count
+	));
+	
+	std::vector<vkcv::DrawcallInfo> drawcallsLines;
+	
+	drawcallsLines.push_back(vkcv::DrawcallInfo(
+			linesMesh,
+			{},
+			1
+	));
+	
+	bool renderGrid = true;
+	
+	float speed_factor = 1.0f;
+	
+	auto start = std::chrono::system_clock::now();
+	auto current = start;
+	
+	while (vkcv::Window::hasOpenWindow()) {
+		vkcv::Window::pollEvents();
+		
+		if (window.getHeight() == 0 || window.getWidth() == 0)
+			continue;
+		
+		uint32_t swapchainWidth, swapchainHeight;
+		if (!core.beginFrame(swapchainWidth, swapchainHeight, windowHandle)) {
+			continue;
+		}
+		
+		if ((swapchainWidth != swapchainExtent.width) || ((swapchainHeight != swapchainExtent.height))) {
+			depthBuffer = core.createImage(
+					vk::Format::eD32Sfloat,
+					swapchainWidth,
+					swapchainHeight
+			).getHandle();
+			
+			swapchainExtent.width = swapchainWidth;
+			swapchainExtent.height = swapchainHeight;
+		}
+		
+		auto next = std::chrono::system_clock::now();
+		
+		auto time = std::chrono::duration_cast<std::chrono::microseconds>(next - start);
+		auto deltatime = std::chrono::duration_cast<std::chrono::microseconds>(next - current);
+		
+		current = next;
+		
+		Physics physics;
+		physics.t = static_cast<float>(0.000001 * static_cast<double>(time.count()));
+		physics.dt = static_cast<float>(0.000001 * static_cast<double>(deltatime.count()));
+		physics.speedfactor = speed_factor;
+		
+		vkcv::PushConstants physicsPushConstants(sizeof(physics));
+		physicsPushConstants.appendDrawcall(physics);
+		
+		cameraManager.update(physics.dt);
+		
+		glm::mat4 mvp = cameraManager.getActiveCamera().getMVP();
+		vkcv::PushConstants cameraPushConstants(sizeof(glm::mat4));
+		cameraPushConstants.appendDrawcall(mvp);
+		
+		auto cmdStream = core.createCommandStream(vkcv::QueueType::Graphics);
+		
+		const uint32_t dispatchSizeGrid[3] = {grid.getWidth() / 4, grid.getHeight() / 4, grid.getDepth() / 4};
+		const uint32_t dispatchSizeParticles[3] = {static_cast<uint32_t>(sim->count + 63) / 64, 1, 1};
+		
+		for (int step = 0; step < 1; step++) {
+			core.recordBeginDebugLabel(cmdStream, "INIT PARTICLE WEIGHTS", {0.78f, 0.89f, 0.94f, 1.0f});
+			core.recordBufferMemoryBarrier(cmdStream, particlesHandle);
+			core.prepareImageForSampling(cmdStream, grid.getHandle());
+			
+			core.recordComputeDispatchToCmdStream(
+					cmdStream,
+					initParticleWeightsPipeline,
+					dispatchSizeParticles,
+					{
+						vkcv::DescriptorSetUsage(
+								0, initParticleWeightsSets[0]
+						),
+						vkcv::DescriptorSetUsage(
+								1, initParticleWeightsSets[1]
+						)
+					},
+					vkcv::PushConstants(0)
+			);
+			
+			core.recordBufferMemoryBarrier(cmdStream, particlesHandle);
+			core.recordEndDebugLabel(cmdStream);
+			
+			core.recordBeginDebugLabel(cmdStream, "TRANSFORM PARTICLES TO GRID", {0.47f, 0.77f, 0.85f, 1.0f});
+			core.recordBufferMemoryBarrier(cmdStream, particlesHandle);
+			core.prepareImageForStorage(cmdStream, grid.getHandle());
+			
+			core.recordComputeDispatchToCmdStream(
+					cmdStream,
+					transformParticlesToGridPipeline,
+					dispatchSizeGrid,
+					{
+						vkcv::DescriptorSetUsage(
+								0, transformParticlesToGridSets[0]
+						),
+						vkcv::DescriptorSetUsage(
+								1, transformParticlesToGridSets[1]
+						),
+						vkcv::DescriptorSetUsage(
+								2, transformParticlesToGridSets[2]
+						)
+					},
+					physicsPushConstants
+			);
+			
+			core.recordImageMemoryBarrier(cmdStream, grid.getHandle());
+			core.recordEndDebugLabel(cmdStream);
+			
+			core.recordBeginDebugLabel(cmdStream, "UPDATE PARTICLE VELOCITIES", {0.78f, 0.89f, 0.94f, 1.0f});
+			core.recordBufferMemoryBarrier(cmdStream, particlesHandle);
+			core.recordBufferMemoryBarrier(cmdStream, simulation.getHandle());
+			core.prepareImageForSampling(cmdStream, grid.getHandle());
+			
+			core.recordComputeDispatchToCmdStream(
+					cmdStream,
+					updateParticleVelocitiesPipeline,
+					dispatchSizeParticles,
+					{
+						vkcv::DescriptorSetUsage(
+								0, updateParticleVelocitiesSets[0]
+						),
+						vkcv::DescriptorSetUsage(
+								1, updateParticleVelocitiesSets[1]
+						),
+						vkcv::DescriptorSetUsage(
+								2, updateParticleVelocitiesSets[2]
+						)
+					},
+					physicsPushConstants
+			);
+			
+			core.recordBufferMemoryBarrier(cmdStream, particlesHandle);
+			core.recordEndDebugLabel(cmdStream);
+		}
+		
+		std::vector<vkcv::ImageHandle> renderTargets {
+				vkcv::ImageHandle::createSwapchainImageHandle(),
+				depthBuffer
+		};
+		
+		if (renderGrid) {
+			core.recordBeginDebugLabel(cmdStream, "RENDER GRID", { 0.13f, 0.20f, 0.22f, 1.0f });
+			core.recordBufferMemoryBarrier(cmdStream, simulation.getHandle());
+			core.prepareImageForSampling(cmdStream, grid.getHandle());
+			
+			core.recordDrawcallsToCmdStream(
+					cmdStream,
+					gfxPassGrid,
+					gfxPipelineGrid,
+					cameraPushConstants,
+					drawcallsGrid,
+					renderTargets,
+					windowHandle
+			);
+			
+			core.recordEndDebugLabel(cmdStream);
+		} else {
+			core.recordBeginDebugLabel(cmdStream, "RENDER PARTICLES", { 0.13f, 0.20f, 0.22f, 1.0f });
+			core.recordBufferMemoryBarrier(cmdStream, particlesHandle);
+			
+			core.recordDrawcallsToCmdStream(
+					cmdStream,
+					gfxPassParticles,
+					gfxPipelineParticles,
+					cameraPushConstants,
+					drawcallsParticles,
+					renderTargets,
+					windowHandle
+			);
+			
+			core.recordEndDebugLabel(cmdStream);
+		}
+		
+		core.recordBeginDebugLabel(cmdStream, "RENDER LINES", { 0.13f, 0.20f, 0.22f, 1.0f });
+		
+		core.recordDrawcallsToCmdStream(
+				cmdStream,
+				gfxPassLines,
+				gfxPipelineLines,
+				cameraPushConstants,
+				drawcallsLines,
+				renderTargets,
+				windowHandle
+		);
+		
+		core.recordEndDebugLabel(cmdStream);
+		
+		core.prepareSwapchainImageForPresent(cmdStream);
+		core.submitCommandStream(cmdStream);
+		
+		gui.beginGUI();
+		ImGui::Begin("Settings");
+		
+		ImGui::BeginGroup();
+		ImGui::Combo("Mode", &(sim->mode), "Random\0Ordered", 2);
+		ImGui::Combo("Form", &(sim->form), "Sphere\0Cube", 2);
+		ImGui::Combo("Type", &(sim->type), "Hyperelastic\0Fluid", 2);
+		ImGui::EndGroup();
+		
+		ImGui::Spacing();
+		
+		ImGui::SliderInt("Particle Count", &(sim->count), 1, 100000);
+		ImGui::SliderFloat("Density", &(sim->density), std::numeric_limits<float>::epsilon(), 5000.0f);
+		ImGui::SameLine(0.0f, 10.0f);
+		if (ImGui::SmallButton("Reset##density")) {
+			sim->density = 2500.0f;
+		}
+		
+		ImGui::SliderFloat("Radius", &(sim->size), 0.0f, 0.5f);
+		ImGui::SameLine(0.0f, 10.0f);
+		if (ImGui::SmallButton("Reset##radius")) {
+			sim->size = 0.1f;
+		}
+		
+		ImGui::Spacing();
+		
+		ImGui::BeginGroup();
+		ImGui::SliderFloat("Bulk Modulus", &(sim->K), 0.0f, 1000.0f);
+		ImGui::SliderFloat("Young's Modulus", &(sim->E), 0.0f, 1000.0f);
+		ImGui::SliderFloat("Heat Capacity Ratio", &(sim->gamma), 1.0f, 2.0f);
+		ImGui::SliderFloat("Lame1", &(sim->lame1), 0.0f, 1000.0f);
+		ImGui::SliderFloat("Lame2", &(sim->lame2), 0.0f, 1000.0f);
+		ImGui::EndGroup();
+
+		ImGui::Spacing();
+
+		ImGui::SliderFloat("Simulation Speed", &speed_factor, 0.0f, 2.0f);
+		
+		ImGui::Spacing();
+		ImGui::Checkbox("Render Grid", &renderGrid);
+		
+		ImGui::DragFloat3("Initial Velocity", reinterpret_cast<float*>(&initialVelocity), 0.001f);
+		ImGui::SameLine(0.0f, 10.0f);
+		if (ImGui::Button("Reset##particle_velocity")) {
+			particlesHandle = resetParticles(
+					core,
+					sim->count,
+					initialVelocity,
+					sim->density,
+					sim->size,
+					sim->form,
+					sim->mode
+			);
+			
+			vkcv::DescriptorWrites writes;
+			writes.storageBufferWrites.push_back(vkcv::BufferDescriptorWrite(0, particlesHandle));
+			
+			core.writeDescriptorSet(initParticleWeightsSets[0], writes);
+			core.writeDescriptorSet(transformParticlesToGridSets[0], writes);
+			core.writeDescriptorSet(updateParticleVelocitiesSets[0], writes);
+			
+			core.writeDescriptorSet(gfxSetParticles, writes);
+		}
+		
+		ImGui::SliderFloat("Gravity", &(sim->gravity), 0.0f, 10.0f);
+		
+		ImGui::End();
+		gui.endGUI();
+		
+		core.endFrame(windowHandle);
+	}
+	
+	simulation.unmap();
+	return 0;
+}
diff --git a/scripts/build.sh b/scripts/build.sh
new file mode 100755
index 0000000000000000000000000000000000000000..7e41ca8e8ad62ce6667ab4d9c549f2ea613d28aa
--- /dev/null
+++ b/scripts/build.sh
@@ -0,0 +1,33 @@
+#!/bin/sh
+# Check if release or debug build
+CMAKE_BUILD_DIR="build"
+CMAKE_FLAGS=""
+if [ "$1" = "--debug" ]; then
+	CMAKE_BUILD_DIR="cmake-build-debug"
+	CMAKE_FLAGS="-DCMAKE_BUILD_TYPE=Debug"
+elif [ "$1" = "--release" ]; then
+	CMAKE_BUILD_DIR="cmake-build-release"
+	CMAKE_FLAGS="-DCMAKE_BUILD_TYPE=Release"
+fi
+
+# Navigate to the main directory of the cloned repository
+cd "$(dirname "$0")" || exit
+cd ..
+
+# Setup git lfs and the submodules
+git lfs install
+git submodule init
+git submodule update
+
+# Setup build directory
+mkdir $CMAKE_BUILD_DIR
+cd $CMAKE_BUILD_DIR || exit
+BUILD_THREADS=$(($(nproc --all) - 1))
+
+if [ $BUILD_THREADS -lt 1 ]; then
+	BUILD_THREADS=1
+fi
+
+# Build process
+cmake $CMAKE_FLAGS ..
+make -j $BUILD_THREADS "$@"
\ No newline at end of file
diff --git a/scripts/generate.sh b/scripts/generate.sh
new file mode 100755
index 0000000000000000000000000000000000000000..cf9ff2d607217890b6fd0586e1dddece962d59b9
--- /dev/null
+++ b/scripts/generate.sh
@@ -0,0 +1,58 @@
+#!/bin/sh
+CMAKE_PROJECT_DIR="$(pwd)"
+CMAKE_PROJECT_NAME="$(basename "$CMAKE_PROJECT_DIR")"
+
+# Navigate to the main directory of the cloned repository
+cd "$(dirname "$0")" || exit
+cd ..
+
+CMAKE_FRAMEWORK_DIR="$(realpath -s --relative-to="$CMAKE_PROJECT_DIR" "$(pwd)")"
+
+# Navigate back to the project directory
+cd "$CMAKE_PROJECT_DIR" || exit
+
+test -f "CMakeLists.txt" && echo "WARNING: CMakeLists.txt exists already! Project generation stops!" && exit
+test -f "src/main.cpp" && echo "WARNING: src/main.cpp exists already! Project generation stops!" && exit
+
+generate_cmake_lists() {
+  echo "cmake_minimum_required(VERSION 3.16)"
+  echo "project($CMAKE_PROJECT_NAME)"
+  echo
+  echo "set(CMAKE_CXX_STANDARD 20)"
+  echo "set(CMAKE_CXX_STANDARD_REQUIRED ON)"
+  echo
+  echo "set(BUILD_MODULES ON CACHE INTERNAL \"\")"
+  echo "set(BUILD_PROJECTS OFF CACHE INTERNAL \"\")"
+  echo "set(BUILD_DOXYGEN_DOCS OFF CACHE INTERNAL \"\")"
+  echo "set(BUILD_SHARED OFF CACHE INTERNAL \"\")"
+  echo "add_subdirectory($CMAKE_FRAMEWORK_DIR)"
+  echo
+  echo "add_executable($CMAKE_PROJECT_NAME src/main.cpp)"
+  echo
+  echo "target_include_directories($CMAKE_PROJECT_NAME SYSTEM BEFORE PRIVATE \${vkcv_includes})"
+  echo "target_link_libraries($CMAKE_PROJECT_NAME \${vkcv_libraries})"
+}
+
+generate_main_cpp() {
+  echo "#include <vkcv/Core.hpp>"
+  echo
+  echo "int main(int argc, const char** argv) {"
+  echo "  vkcv::Core core = vkcv::Core::create("
+  echo "    \"$CMAKE_PROJECT_NAME\","
+  echo "    VK_MAKE_VERSION(0, 0, 1),"
+  echo "    { vk::QueueFlagBits::eTransfer,vk::QueueFlagBits::eGraphics, vk::QueueFlagBits::eCompute },"
+  echo "    { VK_KHR_SWAPCHAIN_EXTENSION_NAME }"
+  echo "  );"
+  echo "  "
+  echo "  vkcv::WindowHandle windowHandle = core.createWindow(\"$CMAKE_PROJECT_NAME\", 800, 600, true);"
+  echo "  "
+  echo "  while (vkcv::Window::hasOpenWindow()) {"
+  echo "    vkcv::Window::pollEvents();"
+  echo "  }"
+  echo "}"
+}
+
+generate_cmake_lists > "CMakeLists.txt"
+
+mkdir -p "src"
+generate_main_cpp > "src/main.cpp"
diff --git a/scripts/release.sh b/scripts/release.sh
new file mode 100755
index 0000000000000000000000000000000000000000..9e4e7a6f6493a07a224054cfb84ae7b20f794e9f
--- /dev/null
+++ b/scripts/release.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+VKCV_BRANCH="$(git status | awk 'NR == 1 { print $3 }')"
+
+if [ "$VKCV_BRANCH" != "develop" ]; then
+	echo "WARNING: Please switch to origin/develop branch for preparing the next release!"
+	exit
+fi
+
+VKCV_VERSION="$(cat include/vkcv/Core.hpp | grep "#define VKCV_FRAMEWORK_VERSION" | awk 'NR == 1 { $1=""; $2=""; print }' | sed -e 's/[^0-9 ]//g' -- | awk '{print $1"."$2"."$3}')"
+
+if [ $(git tag | grep "$VKCV_VERSION" | wc -l) -gt 0 ]; then
+	echo "WARNING: Please adjust the version of the framework before uploading a release! (Duplicate version: $VKCV_VERSION)"
+	exit
+fi
+
+vim CHANGELOG.md
+git add CHANGELOG.md
+
+git commit -S -s -m "==========  VkCV-$VKCV_VERSION  =========="
+git push
+
+git checkout master
+git merge develop
+
+git tag -a "$VKCV_VERSION" -m "VkCV-$VKCV_VERSION release"
+git push
+git push --tags
diff --git a/scripts/run.sh b/scripts/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..2f5a32131003b8c684adb4a4d0d9219bdc8fc3d8
--- /dev/null
+++ b/scripts/run.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+# Navigate to the scripts directory
+cd "$(dirname "$0")" || exit
+
+# Check if there is a project name as argument
+if [ $# -lt 1 ]; then
+	echo "You need to specify a project name to run!">&2
+	exit
+fi
+
+RUN_WITH_HUD="no"
+BUILD_FLAGS=""
+while [ $# -gt 1 ]; do
+  case "$1" in
+    "--"*)
+      if [ "$1" = "--hud" ]; then
+        RUN_WITH_HUD="yes"
+      else
+        BUILD_FLAGS="$BUILD_FLAGS$1 "
+      fi
+      shift 1;;
+    *) break;;
+  esac
+done
+
+PROJECT="$1"
+PROJECT_DIR="../projects/$PROJECT"
+shift 1
+
+# Check if the project name is valid
+if [ ! -d "$PROJECT_DIR" ]; then
+	echo "There is no project with the name '$PROJECT'!">&2
+	exit
+fi
+
+./build.sh $FLAGS "$PROJECT"
+cd "$PROJECT_DIR" || exit
+
+if [ ! -f "$PROJECT" ]; then
+	echo "Building the project '$PROJECT' failed!">&2
+	exit
+fi
+
+if [ "$RUN_WITH_HUD" = "yes" ]; then
+  MANGOHUD=1 ./"$PROJECT" "$@"
+else
+  ./"$PROJECT" "$@"
+fi
\ No newline at end of file
diff --git a/src/vkcv/ComputePipelineManager.cpp b/src/vkcv/ComputePipelineManager.cpp
index f090b6dbe7e05f488958c072a758fbf2d2938ab5..a599001d3c60e126cfa0a52a1347abfe48b961a9 100644
--- a/src/vkcv/ComputePipelineManager.cpp
+++ b/src/vkcv/ComputePipelineManager.cpp
@@ -41,11 +41,12 @@ namespace vkcv
         return pipeline.m_layout;
     }
 
-    ComputePipelineHandle ComputePipelineManager::createComputePipeline(const ComputePipelineConfig& config) {
+    ComputePipelineHandle ComputePipelineManager::createComputePipeline(const ShaderProgram& shaderProgram,
+																		const std::vector<vk::DescriptorSetLayout>& descriptorSetLayouts) {
 
         // Temporally handing over the Shader Program instead of a pipeline config
         vk::ShaderModule computeModule{};
-        if (createShaderModule(computeModule, config.m_ShaderProgram, ShaderStage::COMPUTE) != vk::Result::eSuccess)
+        if (createShaderModule(computeModule, shaderProgram, ShaderStage::COMPUTE) != vk::Result::eSuccess)
             return ComputePipelineHandle();
 
         vk::PipelineShaderStageCreateInfo pipelineComputeShaderStageInfo(
@@ -56,9 +57,9 @@ namespace vkcv
                 nullptr
         );
 
-        vk::PipelineLayoutCreateInfo pipelineLayoutCreateInfo({}, config.m_DescriptorSetLayouts);
+        vk::PipelineLayoutCreateInfo pipelineLayoutCreateInfo({}, descriptorSetLayouts);
 
-        const size_t pushConstantSize = config.m_ShaderProgram.getPushConstantSize();
+        const size_t pushConstantSize = shaderProgram.getPushConstantSize();
         vk::PushConstantRange pushConstantRange(vk::ShaderStageFlagBits::eCompute, 0, pushConstantSize);
         if (pushConstantSize > 0) {
             pipelineLayoutCreateInfo.setPushConstantRangeCount(1);
diff --git a/src/vkcv/ComputePipelineManager.hpp b/src/vkcv/ComputePipelineManager.hpp
index 527243e6ec43850d416a7d929e01351454c40086..acb50bbdef99d037374c140836d25bb161132b2b 100644
--- a/src/vkcv/ComputePipelineManager.hpp
+++ b/src/vkcv/ComputePipelineManager.hpp
@@ -1,7 +1,7 @@
 #pragma once
 
 /**
- * @authors Mark Mints
+ * @authors Mark Mints, Tobias Frisch
  * @file src/vkcv/ComputePipelineManager.hpp
  * @brief Creation and handling of Compute Pipelines
  */
@@ -49,10 +49,12 @@ namespace vkcv
          * Creates a Compute Pipeline based on the set shader stages in the Config Struct.
          * This function is wrapped in /src/vkcv/Core.cpp by Core::createComputePipeline(const ComputePipelineConfig &config).
          * On application level it is necessary first to fill a ComputePipelineConfig Struct.
-         * @param config Hands over all needed information for pipeline creation.
+         * @param shaderProgram Hands over all needed information for pipeline creation.
+         * @param descriptorSetLayouts Hands over all needed information for pipeline creation.
          * @return A Handler to the created Compute Pipeline Object.
          */
-        ComputePipelineHandle createComputePipeline(const ComputePipelineConfig& config);
+        ComputePipelineHandle createComputePipeline(const ShaderProgram& shaderProgram,
+													const std::vector<vk::DescriptorSetLayout>& descriptorSetLayouts);
 
     private:
         struct ComputePipeline {
diff --git a/src/vkcv/Context.cpp b/src/vkcv/Context.cpp
index 2f3453f3ff97bd778df8fcfd36d79cc901c867ba..f7cd67eb756f353d9eb98c4768e2b0d74bbc1ab6 100644
--- a/src/vkcv/Context.cpp
+++ b/src/vkcv/Context.cpp
@@ -1,6 +1,7 @@
 
 #include "vkcv/Context.hpp"
 #include "vkcv/Window.hpp"
+#include "vkcv/Core.hpp"
 
 namespace vkcv
 {
@@ -239,8 +240,8 @@ namespace vkcv
 		const vk::ApplicationInfo applicationInfo(
 				applicationName,
 				applicationVersion,
-				"vkCV",
-				VK_MAKE_VERSION(0, 0, 1),
+				VKCV_FRAMEWORK_NAME,
+				VKCV_FRAMEWORK_VERSION,
 				VK_HEADER_VERSION_COMPLETE
 		);
 		
diff --git a/src/vkcv/Core.cpp b/src/vkcv/Core.cpp
index bf24089e935302767f2a6b947d68748db8b0313d..6964884361ad9d3956c7e4333a7189680b9099da 100644
--- a/src/vkcv/Core.cpp
+++ b/src/vkcv/Core.cpp
@@ -80,12 +80,19 @@ namespace vkcv
 	
 	GraphicsPipelineHandle Core::createGraphicsPipeline(const GraphicsPipelineConfig &config)
     {
-        return m_PipelineManager->createPipeline(config, *m_PassManager);
+        return m_PipelineManager->createPipeline(config, *m_PassManager, *m_DescriptorManager);
     }
 
     ComputePipelineHandle Core::createComputePipeline(const ComputePipelineConfig &config)
     {
-        return m_ComputePipelineManager->createComputePipeline(config);
+		std::vector<vk::DescriptorSetLayout> layouts;
+		layouts.resize(config.m_DescriptorSetLayouts.size());
+	
+		for (size_t i = 0; i < layouts.size(); i++) {
+			layouts[i] = getDescriptorSetLayout(config.m_DescriptorSetLayouts[i]).vulkanHandle;
+		}
+		
+        return m_ComputePipelineManager->createComputePipeline(config.m_ShaderProgram, layouts);
     }
 
     PassHandle Core::createPass(const PassConfig &config)
@@ -261,6 +268,56 @@ namespace vkcv
 		cmdBuffer.setViewport(0, 1, &dynamicViewport);
 		cmdBuffer.setScissor(0, 1, &dynamicScissor);
 	}
+	
+	vk::IndexType getIndexType(IndexBitCount indexByteCount){
+		switch (indexByteCount) {
+			case IndexBitCount::Bit16: return vk::IndexType::eUint16;
+			case IndexBitCount::Bit32: return vk::IndexType::eUint32;
+			default:
+			vkcv_log(LogLevel::ERROR, "unknown Enum");
+				return vk::IndexType::eUint16;
+		}
+	}
+	
+	void recordDrawcall(
+			const Core				&core,
+			const DrawcallInfo      &drawcall,
+			vk::CommandBuffer       cmdBuffer,
+			vk::PipelineLayout      pipelineLayout,
+			const PushConstants     &pushConstants,
+			const size_t            drawcallIndex) {
+		
+		for (uint32_t i = 0; i < drawcall.mesh.vertexBufferBindings.size(); i++) {
+			const auto& vertexBinding = drawcall.mesh.vertexBufferBindings[i];
+			cmdBuffer.bindVertexBuffers(i, vertexBinding.buffer, vertexBinding.offset);
+		}
+		
+		for (const auto& descriptorUsage : drawcall.descriptorSets) {
+			cmdBuffer.bindDescriptorSets(
+					vk::PipelineBindPoint::eGraphics,
+					pipelineLayout,
+					descriptorUsage.setLocation,
+					core.getDescriptorSet(descriptorUsage.descriptorSet).vulkanHandle,
+					nullptr);
+		}
+		
+		if (pushConstants.getSizePerDrawcall() > 0) {
+			cmdBuffer.pushConstants(
+					pipelineLayout,
+					vk::ShaderStageFlagBits::eAll,
+					0,
+					pushConstants.getSizePerDrawcall(),
+					pushConstants.getDrawcallData(drawcallIndex));
+		}
+		
+		if (drawcall.mesh.indexBuffer) {
+			cmdBuffer.bindIndexBuffer(drawcall.mesh.indexBuffer, 0, getIndexType(drawcall.mesh.indexBitCount));
+			cmdBuffer.drawIndexed(drawcall.mesh.indexCount, drawcall.instanceCount, 0, 0, {});
+		}
+		else {
+			cmdBuffer.draw(drawcall.mesh.indexCount, drawcall.instanceCount, 0, 0, {});
+		}
+	}
 
 	void Core::recordDrawcallsToCmdStream(
 		const CommandStreamHandle&      cmdStreamHandle,
@@ -316,7 +373,7 @@ namespace vkcv
 			}
 
 			for (size_t i = 0; i < drawcalls.size(); i++) {
-				recordDrawcall(drawcalls[i], cmdBuffer, pipelineLayout, pushConstantData, i);
+				recordDrawcall(*this, drawcalls[i], cmdBuffer, pipelineLayout, pushConstantData, i);
 			}
 
 			cmdBuffer.endRenderPass();
@@ -483,6 +540,7 @@ namespace vkcv
 			for (size_t i = 0; i < drawcalls.size(); i++) {
                 const uint32_t pushConstantOffset = i * pushConstantData.getSizePerDrawcall();
                 recordMeshShaderDrawcall(
+					*this,
                     cmdBuffer,
                     pipelineLayout,
                     pushConstantData,
@@ -522,7 +580,7 @@ namespace vkcv
 					vk::PipelineBindPoint::eRayTracingKHR,
 					rtxPipelineLayout,
 					usage.setLocation,
-					{ usage.vulkanHandle },
+					{ getDescriptorSet(usage.descriptorSet).vulkanHandle },
 					usage.dynamicOffsets
 				);
 			}
@@ -563,7 +621,7 @@ namespace vkcv
 					vk::PipelineBindPoint::eCompute,
 					pipelineLayout,
 					usage.setLocation,
-					{ usage.vulkanHandle },
+					{ getDescriptorSet(usage.descriptorSet).vulkanHandle },
 					usage.dynamicOffsets
 				);
 			}
@@ -642,7 +700,7 @@ namespace vkcv
 					vk::PipelineBindPoint::eCompute,
 					pipelineLayout,
 					usage.setLocation,
-					{ usage.vulkanHandle },
+					{ getDescriptorSet(usage.descriptorSet).vulkanHandle },
 					usage.dynamicOffsets
 				);
 			}
@@ -756,8 +814,8 @@ namespace vkcv
 
 	SamplerHandle Core::createSampler(SamplerFilterType magFilter, SamplerFilterType minFilter,
 									  SamplerMipmapMode mipmapMode, SamplerAddressMode addressMode,
-									  float mipLodBias) {
-		return m_SamplerManager->createSampler(magFilter, minFilter, mipmapMode, addressMode, mipLodBias);
+									  float mipLodBias, SamplerBorderColor borderColor) {
+		return m_SamplerManager->createSampler(magFilter, minFilter, mipmapMode, addressMode, mipLodBias, borderColor);
 	}
 
 	Image Core::createImage(
diff --git a/src/vkcv/DescriptorManager.hpp b/src/vkcv/DescriptorManager.hpp
index acc6edd9b704c377480e7acbbc40b3e763003d5f..d60a6d5c6eb1a66db07487c888505e8b3731b046 100644
--- a/src/vkcv/DescriptorManager.hpp
+++ b/src/vkcv/DescriptorManager.hpp
@@ -1,3 +1,5 @@
+#pragma once
+
 /**
  * @authors Artur Wasmut, Susanne D�tsch, Simeon Hermann
  * @file src/vkcv/DescriptorManager.cpp
@@ -21,7 +23,7 @@ namespace vkcv
 	    explicit DescriptorManager(vk::Device device) noexcept;
 	    ~DescriptorManager() noexcept;
 
-	    DescriptorSetLayoutHandle createDescriptorSetLayout(const std::unordered_map<uint32_t, DescriptorBinding> &setBindingsMap);
+	    DescriptorSetLayoutHandle createDescriptorSetLayout(const DescriptorBindings &setBindingsMap);
         DescriptorSetHandle createDescriptorSet(const DescriptorSetLayoutHandle &setLayoutHandle);
 
 		void writeDescriptorSet(
diff --git a/src/vkcv/DrawcallRecording.cpp b/src/vkcv/DrawcallRecording.cpp
index 638df388178f79f680c3b9f797d9d435e752fe10..e13b4d24148e9d76d8be4ad7fd3db0db87249c59 100644
--- a/src/vkcv/DrawcallRecording.cpp
+++ b/src/vkcv/DrawcallRecording.cpp
@@ -1,67 +1,9 @@
-#include <vkcv/DrawcallRecording.hpp>
-#include <vkcv/Logger.hpp>
 
-namespace vkcv {
-
-    vk::IndexType getIndexType(IndexBitCount indexByteCount){
-        switch (indexByteCount) {
-            case IndexBitCount::Bit16: return vk::IndexType::eUint16;
-            case IndexBitCount::Bit32: return vk::IndexType::eUint32;
-            default:
-                vkcv_log(LogLevel::ERROR, "unknown Enum");
-                return vk::IndexType::eUint16;
-        }
-    }
-
-    void recordDrawcall(
-        const DrawcallInfo      &drawcall,
-        vk::CommandBuffer       cmdBuffer,
-        vk::PipelineLayout      pipelineLayout,
-        const PushConstants     &pushConstants,
-        const size_t            drawcallIndex) {
-
-        for (uint32_t i = 0; i < drawcall.mesh.vertexBufferBindings.size(); i++) {
-            const auto& vertexBinding = drawcall.mesh.vertexBufferBindings[i];
-            cmdBuffer.bindVertexBuffers(i, vertexBinding.buffer, vertexBinding.offset);
-        }
+#include "vkcv/DrawcallRecording.hpp"
+#include "vkcv/Logger.hpp"
+#include "vkcv/Core.hpp"
 
-        for (const auto& descriptorUsage : drawcall.descriptorSets) {
-            cmdBuffer.bindDescriptorSets(
-                vk::PipelineBindPoint::eGraphics,
-                pipelineLayout,
-                descriptorUsage.setLocation,
-                descriptorUsage.vulkanHandle,
-                nullptr);
-        }
-
-        if (pushConstants.getSizePerDrawcall() > 0) {
-            cmdBuffer.pushConstants(
-                pipelineLayout,
-                vk::ShaderStageFlagBits::eAll,
-                0,
-				pushConstants.getSizePerDrawcall(),
-                pushConstants.getDrawcallData(drawcallIndex));
-        }
-
-        if (drawcall.mesh.indexBuffer) {
-            cmdBuffer.bindIndexBuffer(drawcall.mesh.indexBuffer, 0, getIndexType(drawcall.mesh.indexBitCount));
-            cmdBuffer.drawIndexed(drawcall.mesh.indexCount, drawcall.instanceCount, 0, 0, {});
-        }
-        else {
-            cmdBuffer.draw(drawcall.mesh.indexCount, drawcall.instanceCount, 0, 0, {});
-        }
-    }
-
-    void recordIndirectDrawcall(
-            const DrawcallInfo                              &drawcall,
-            vk::CommandBuffer                               cmdBuffer,
-            const Buffer <vk::DrawIndexedIndirectCommand>   &drawBuffer,
-            const uint32_t                                  drawCount,
-            vk::PipelineLayout                              pipelineLayout,
-            const PushConstants                             &pushConstants,
-            const size_t                                    drawcallIndex) {
-        return;
-    }
+namespace vkcv {
 
     struct MeshShaderFunctions
     {
@@ -78,6 +20,7 @@ namespace vkcv {
     }
 
     void recordMeshShaderDrawcall(
+		const Core&								core,
         vk::CommandBuffer                       cmdBuffer,
         vk::PipelineLayout                      pipelineLayout,
         const PushConstants&                    pushConstantData,
@@ -90,7 +33,7 @@ namespace vkcv {
                 vk::PipelineBindPoint::eGraphics,
                 pipelineLayout,
                 descriptorUsage.setLocation,
-                descriptorUsage.vulkanHandle,
+				core.getDescriptorSet(descriptorUsage.descriptorSet).vulkanHandle,
                 nullptr);
         }
 
diff --git a/src/vkcv/FeatureManager.cpp b/src/vkcv/FeatureManager.cpp
index 4aefcb90131b5f9d6144d1f870b6fe709cd6c1e7..af4426252d840bdffd7d210d900be64d6b74f6f3 100644
--- a/src/vkcv/FeatureManager.cpp
+++ b/src/vkcv/FeatureManager.cpp
@@ -296,6 +296,44 @@ m_physicalDevice.getFeatures2(&query)
 		return true;
 	}
 	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceShaderAtomicFloatFeaturesEXT& features, bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceShaderAtomicFloatFeaturesEXT);
+		
+		vkcv_check_feature(shaderBufferFloat32Atomics);
+		vkcv_check_feature(shaderBufferFloat32AtomicAdd);
+		vkcv_check_feature(shaderBufferFloat64Atomics);
+		vkcv_check_feature(shaderBufferFloat64AtomicAdd);
+		vkcv_check_feature(shaderSharedFloat32Atomics);
+		vkcv_check_feature(shaderSharedFloat32AtomicAdd);
+		vkcv_check_feature(shaderSharedFloat64Atomics);
+		vkcv_check_feature(shaderSharedFloat64AtomicAdd);
+		vkcv_check_feature(shaderImageFloat32Atomics);
+		vkcv_check_feature(shaderImageFloat32AtomicAdd);
+		vkcv_check_feature(sparseImageFloat32Atomics);
+		vkcv_check_feature(sparseImageFloat32AtomicAdd);
+		
+		return true;
+	}
+	
+	bool FeatureManager::checkSupport(const vk::PhysicalDeviceShaderAtomicFloat2FeaturesEXT& features, bool required) const {
+		vkcv_check_init_features2(vk::PhysicalDeviceShaderAtomicFloat2FeaturesEXT);
+		
+		vkcv_check_feature(shaderBufferFloat16Atomics);
+		vkcv_check_feature(shaderBufferFloat16AtomicAdd);
+		vkcv_check_feature(shaderBufferFloat16AtomicMinMax);
+		vkcv_check_feature(shaderBufferFloat32AtomicMinMax);
+		vkcv_check_feature(shaderBufferFloat64AtomicMinMax);
+		vkcv_check_feature(shaderSharedFloat16Atomics);
+		vkcv_check_feature(shaderSharedFloat16AtomicAdd);
+		vkcv_check_feature(shaderSharedFloat16AtomicMinMax);
+		vkcv_check_feature(shaderSharedFloat32AtomicMinMax);
+		vkcv_check_feature(shaderSharedFloat64AtomicMinMax);
+		vkcv_check_feature(shaderImageFloat32AtomicMinMax);
+		vkcv_check_feature(sparseImageFloat32AtomicMinMax);
+		
+		return true;
+	}
+	
 	bool FeatureManager::checkSupport(const vk::PhysicalDeviceVulkan12Features &features, bool required) const {
 	    vkcv_check_init_features2(vk::PhysicalDeviceVulkan12Features);
 
diff --git a/src/vkcv/GraphicsPipelineManager.cpp b/src/vkcv/GraphicsPipelineManager.cpp
index a461051f9784e7d95de7c15c965caa633d3bb35c..ab0d9f26fdf482698f679b598d5b087e3674c638 100644
--- a/src/vkcv/GraphicsPipelineManager.cpp
+++ b/src/vkcv/GraphicsPipelineManager.cpp
@@ -51,6 +51,8 @@ namespace vkcv
                 return vk::PrimitiveTopology::eLineList;
             case(PrimitiveTopology::TriangleList):
                 return vk::PrimitiveTopology::eTriangleList;
+			case(PrimitiveTopology::PatchList):
+				return vk::PrimitiveTopology::ePatchList;
             default:
             vkcv_log(LogLevel::ERROR, "Unknown primitive topology type");
                 return vk::PrimitiveTopology::eTriangleList;
@@ -195,6 +197,15 @@ namespace vkcv
 		return pipelineInputAssemblyStateCreateInfo;
 	}
 	
+	vk::PipelineTessellationStateCreateInfo createPipelineTessellationStateCreateInfo(const GraphicsPipelineConfig &config) {
+		vk::PipelineTessellationStateCreateInfo pipelineTessellationStateCreateInfo(
+				{},
+				config.m_tessellationControlPoints
+		);
+		
+		return pipelineTessellationStateCreateInfo;
+	}
+	
 	/**
 	 * Creates a Pipeline Viewport State Create Info Struct with default set viewport and scissor settings.
 	 * @param config provides with and height of the output window
@@ -350,7 +361,8 @@ namespace vkcv
 	 * @param config sets Push Constant Size and Descriptor Layouts.
 	 * @return Pipeline Layout Create Info Struct
 	 */
-	vk::PipelineLayoutCreateInfo createPipelineLayoutCreateInfo(const GraphicsPipelineConfig &config) {
+	vk::PipelineLayoutCreateInfo createPipelineLayoutCreateInfo(const GraphicsPipelineConfig &config,
+																const std::vector<vk::DescriptorSetLayout>& descriptorSetLayouts) {
 		static vk::PushConstantRange pushConstantRange;
 		
 		const size_t pushConstantSize = config.m_ShaderProgram.getPushConstantSize();
@@ -360,7 +372,7 @@ namespace vkcv
 		
 		vk::PipelineLayoutCreateInfo pipelineLayoutCreateInfo(
 				{},
-				(config.m_DescriptorLayouts),
+				(descriptorSetLayouts),
 				(pushConstantRange)
 		);
 		
@@ -416,7 +428,9 @@ namespace vkcv
 		return dynamicStateCreateInfo;
 	}
 
-    GraphicsPipelineHandle GraphicsPipelineManager::createPipeline(const GraphicsPipelineConfig &config, PassManager& passManager) {
+    GraphicsPipelineHandle GraphicsPipelineManager::createPipeline(const GraphicsPipelineConfig &config,
+																   const PassManager& passManager,
+																   const DescriptorManager& descriptorManager) {
         const vk::RenderPass &pass = passManager.getVkPass(config.m_PassHandle);
 
         const bool existsTaskShader     = config.m_ShaderProgram.existsShader(ShaderStage::TASK);
@@ -424,7 +438,13 @@ namespace vkcv
         const bool existsVertexShader   = config.m_ShaderProgram.existsShader(ShaderStage::VERTEX);
         const bool existsFragmentShader = config.m_ShaderProgram.existsShader(ShaderStage::FRAGMENT);
         const bool existsGeometryShader = config.m_ShaderProgram.existsShader(ShaderStage::GEOMETRY);
-        const bool validGeometryStages  = existsVertexShader || (existsTaskShader && existsMeshShader);
+		const bool existsTessellationControlShader = config.m_ShaderProgram.existsShader(ShaderStage::TESS_CONTROL);
+		const bool existsTessellationEvaluationShader = config.m_ShaderProgram.existsShader(ShaderStage::TESS_EVAL);
+		
+        const bool validGeometryStages  = (
+				(existsVertexShader && (existsTessellationControlShader == existsTessellationEvaluationShader)) ||
+				(existsTaskShader && existsMeshShader)
+		);
 
         if (!validGeometryStages)
         {
@@ -529,6 +549,40 @@ namespace vkcv
                 return GraphicsPipelineHandle();
             }
         }
+	
+		if (existsTessellationControlShader) {
+			vk::PipelineShaderStageCreateInfo createInfo;
+			const bool success = createPipelineShaderStageCreateInfo(
+					config.m_ShaderProgram,
+					ShaderStage::TESS_CONTROL,
+					m_Device,
+					&createInfo);
+		
+			if (success) {
+				shaderStages.push_back(createInfo);
+			}
+			else {
+				destroyShaderModules();
+				return GraphicsPipelineHandle();
+			}
+		}
+	
+		if (existsTessellationEvaluationShader) {
+			vk::PipelineShaderStageCreateInfo createInfo;
+			const bool success = createPipelineShaderStageCreateInfo(
+					config.m_ShaderProgram,
+					ShaderStage::TESS_EVAL,
+					m_Device,
+					&createInfo);
+		
+			if (success) {
+				shaderStages.push_back(createInfo);
+			}
+			else {
+				destroyShaderModules();
+				return GraphicsPipelineHandle();
+			}
+		}
 
         // vertex input state
         // Fill up VertexInputBindingDescription and VertexInputAttributeDescription Containers
@@ -545,6 +599,10 @@ namespace vkcv
         vk::PipelineInputAssemblyStateCreateInfo pipelineInputAssemblyStateCreateInfo =
                 createPipelineInputAssemblyStateCreateInfo(config);
 
+		// tesselation state
+		vk::PipelineTessellationStateCreateInfo pipelineTessellationStateCreateInfo =
+				createPipelineTessellationStateCreateInfo(config);
+		
         // viewport state
         vk::PipelineViewportStateCreateInfo pipelineViewportStateCreateInfo =
                 createPipelineViewportStateCreateInfo(config);
@@ -570,9 +628,15 @@ namespace vkcv
         vk::PipelineDynamicStateCreateInfo dynamicStateCreateInfo =
                 createPipelineDynamicStateCreateInfo(config);
 
+		std::vector<vk::DescriptorSetLayout> descriptorSetLayouts;
+		descriptorSetLayouts.reserve(config.m_DescriptorLayouts.size());
+		for (const auto& handle : config.m_DescriptorLayouts) {
+			descriptorSetLayouts.push_back(descriptorManager.getDescriptorSetLayout(handle).vulkanHandle);
+		}
+		
         // pipeline layout
         vk::PipelineLayoutCreateInfo pipelineLayoutCreateInfo =
-                createPipelineLayoutCreateInfo(config);
+                createPipelineLayoutCreateInfo(config, descriptorSetLayouts);
 
         vk::PipelineLayout vkPipelineLayout{};
         if (m_Device.createPipelineLayout(&pipelineLayoutCreateInfo, nullptr, &vkPipelineLayout) != vk::Result::eSuccess) {
@@ -601,7 +665,7 @@ namespace vkcv
                 shaderStages.data(),
                 &pipelineVertexInputStateCreateInfo,
                 &pipelineInputAssemblyStateCreateInfo,
-                nullptr,
+                &pipelineTessellationStateCreateInfo,
                 &pipelineViewportStateCreateInfo,
                 &pipelineRasterizationStateCreateInfo,
                 &pipelineMultisampleStateCreateInfo,
diff --git a/src/vkcv/GraphicsPipelineManager.hpp b/src/vkcv/GraphicsPipelineManager.hpp
index 782603ab0e1ffa9bde05fda96c5d2d259eff1953..09900c37760f07d1967aa56a75cfc8b94aa289d5 100644
--- a/src/vkcv/GraphicsPipelineManager.hpp
+++ b/src/vkcv/GraphicsPipelineManager.hpp
@@ -13,6 +13,7 @@
 #include "vkcv/Handles.hpp"
 #include "vkcv/GraphicsPipelineConfig.hpp"
 #include "PassManager.hpp"
+#include "DescriptorManager.hpp"
 
 namespace vkcv
 {
@@ -36,9 +37,12 @@ namespace vkcv
          * On application level it is necessary first to fill a PipelineConfig Struct.
          * @param config Hands over all needed information for pipeline creation.
          * @param passManager Hands over the corresponding render pass.
+         * @param descriptorManager Hands over the corresponding descriptor set layouts
          * @return A Handler to the created Graphics Pipeline Object.
          */
-		GraphicsPipelineHandle createPipeline(const GraphicsPipelineConfig &config, PassManager& passManager);
+		GraphicsPipelineHandle createPipeline(const GraphicsPipelineConfig &config,
+											  const PassManager& passManager,
+											  const DescriptorManager& descriptorManager);
 
         /**
          * Returns a vk::Pipeline object by handle.
diff --git a/src/vkcv/ImageManager.cpp b/src/vkcv/ImageManager.cpp
index bde020498e19e3f9bf0667c7182ca13d11f9044f..bfee504f599d941e625e6ef5c6045805c33a29a1 100644
--- a/src/vkcv/ImageManager.cpp
+++ b/src/vkcv/ImageManager.cpp
@@ -345,7 +345,7 @@ namespace vkcv {
 		recordImageBarrier(cmdBuffer, transitionBarrier);
 	}
 	
-	constexpr uint32_t getChannelsByFormat(vk::Format format) {
+	constexpr uint32_t getBytesPerPixel(vk::Format format) {
 		switch (format) {
 			case vk::Format::eR8Unorm:
 				return 1;
@@ -353,6 +353,10 @@ namespace vkcv {
 				return 4;
 			case vk::Format::eR8G8B8A8Unorm:
 				return 4;
+			case vk::Format::eR16G16B16A16Sfloat:
+				return 8;
+			case vk::Format::eR32G32B32A32Sfloat:
+				return 16;
 			default:
 				std::cerr << "Unknown image format" << std::endl;
 				return 4;
@@ -379,15 +383,15 @@ namespace vkcv {
 				handle,
 				vk::ImageLayout::eTransferDstOptimal);
 		
-		uint32_t channels = getChannelsByFormat(image.m_format);
 		const size_t image_size = (
-				image.m_width * image.m_height * image.m_depth * channels
+				image.m_width * image.m_height * image.m_depth *
+				getBytesPerPixel(image.m_format)
 		);
 		
 		const size_t max_size = std::min(size, image_size);
 		
 		BufferHandle bufferHandle = m_bufferManager.createBuffer(
-				BufferType::STAGING, max_size, BufferMemoryType::HOST_VISIBLE, false
+				BufferType::STAGING, max_size, BufferMemoryType::DEVICE_LOCAL, false
 		);
 		
 		m_bufferManager.fillBuffer(bufferHandle, data, max_size, 0);
diff --git a/src/vkcv/PassConfig.cpp b/src/vkcv/PassConfig.cpp
index 78bd5808b63fee7333243db4fca640047f76eae9..b203fab4f02afcc8f51b468ab4504480a8e60e4e 100644
--- a/src/vkcv/PassConfig.cpp
+++ b/src/vkcv/PassConfig.cpp
@@ -11,7 +11,7 @@ namespace vkcv
 	store_operation{store_op},
 	load_operation{load_op},
 	format(format)
-    {};
+    {}
 
     PassConfig::PassConfig(std::vector<AttachmentDescription> attachments, Multisampling msaa) noexcept :
     attachments{std::move(attachments) }, msaa(msaa)
diff --git a/src/vkcv/SamplerManager.cpp b/src/vkcv/SamplerManager.cpp
index 792e6f16b4a05af41a164a1eda9dd7423594857e..9a80635744e5a3dd0b6bd8db476cec841b1c317d 100644
--- a/src/vkcv/SamplerManager.cpp
+++ b/src/vkcv/SamplerManager.cpp
@@ -18,11 +18,13 @@ namespace vkcv {
 												SamplerFilterType minFilter,
 												SamplerMipmapMode mipmapMode,
 												SamplerAddressMode addressMode,
-												float mipLodBias) {
+												float mipLodBias,
+												SamplerBorderColor borderColor) {
 		vk::Filter vkMagFilter;
 		vk::Filter vkMinFilter;
 		vk::SamplerMipmapMode vkMipmapMode;
 		vk::SamplerAddressMode vkAddressMode;
+		vk::BorderColor vkBorderColor;
 		
 		switch (magFilter) {
 			case SamplerFilterType::NEAREST:
@@ -70,6 +72,32 @@ namespace vkcv {
 			case SamplerAddressMode::MIRROR_CLAMP_TO_EDGE:
 				vkAddressMode = vk::SamplerAddressMode::eMirrorClampToEdge;
 				break;
+			case SamplerAddressMode::CLAMP_TO_BORDER:
+				vkAddressMode = vk::SamplerAddressMode::eClampToBorder;
+				break;
+			default:
+				return SamplerHandle();
+		}
+		
+		switch (borderColor) {
+			case SamplerBorderColor::INT_ZERO_OPAQUE:
+				vkBorderColor = vk::BorderColor::eIntOpaqueBlack;
+				break;
+			case SamplerBorderColor::INT_ZERO_TRANSPARENT:
+				vkBorderColor = vk::BorderColor::eIntTransparentBlack;
+				break;
+			case SamplerBorderColor::FLOAT_ZERO_OPAQUE:
+				vkBorderColor = vk::BorderColor::eFloatOpaqueBlack;
+				break;
+			case SamplerBorderColor::FLOAT_ZERO_TRANSPARENT:
+				vkBorderColor = vk::BorderColor::eFloatTransparentBlack;
+				break;
+			case SamplerBorderColor::INT_ONE_OPAQUE:
+				vkBorderColor = vk::BorderColor::eIntOpaqueWhite;
+				break;
+			case SamplerBorderColor::FLOAT_ONE_OPAQUE:
+				vkBorderColor = vk::BorderColor::eFloatOpaqueWhite;
+				break;
 			default:
 				return SamplerHandle();
 		}
@@ -89,7 +117,7 @@ namespace vkcv {
 				vk::CompareOp::eAlways,
 				-1000.0f,
 				1000.0f,
-				vk::BorderColor::eIntOpaqueBlack,
+				vkBorderColor,
 				false
 		);
 		
diff --git a/src/vkcv/SamplerManager.hpp b/src/vkcv/SamplerManager.hpp
index aea47a03714b417314a09dfc0be855df31fbb557..128faa711993ac052cf774a1d31144d19362658f 100644
--- a/src/vkcv/SamplerManager.hpp
+++ b/src/vkcv/SamplerManager.hpp
@@ -30,10 +30,11 @@ namespace vkcv {
 		SamplerManager& operator=(SamplerManager&& other) = delete;
 		
 		SamplerHandle createSampler(SamplerFilterType magFilter,
-							  		SamplerFilterType minFilter,
-							  		SamplerMipmapMode mipmapMode,
-							  		SamplerAddressMode addressMode,
-							  		float mipLodBias);
+									SamplerFilterType minFilter,
+									SamplerMipmapMode mipmapMode,
+									SamplerAddressMode addressMode,
+									float mipLodBias,
+									SamplerBorderColor borderColor);
 		
 		[[nodiscard]]
 		vk::Sampler getVulkanSampler(const SamplerHandle& handle) const;