From 28c3662459fcd07863e218bc3ddf0092541cf876 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 24 Feb 2025 09:49:48 +0100 Subject: [PATCH 001/175] init Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/.gitignore | 0 Gems/FPSProfiler/CMakeLists.txt | 6 + Gems/FPSProfiler/Code/CMakeLists.txt | 257 ++++++++++++++++++ .../Code/Include/FPSProfiler/FPSProfilerBus.h | 33 +++ .../Include/FPSProfiler/FPSProfilerTypeIds.h | 19 ++ .../Code/Platform/Android/PAL_android.cmake | 4 + .../Android/fpsprofiler_api_files.cmake | 3 + .../Android/fpsprofiler_private_files.cmake | 8 + .../Android/fpsprofiler_shared_files.cmake | 8 + .../Code/Platform/Linux/PAL_linux.cmake | 4 + .../Linux/fpsprofiler_api_files.cmake | 3 + .../Linux/fpsprofiler_editor_api_files.cmake | 3 + .../Linux/fpsprofiler_private_files.cmake | 8 + .../Linux/fpsprofiler_shared_files.cmake | 8 + .../Code/Platform/Mac/PAL_mac.cmake | 4 + .../Platform/Mac/fpsprofiler_api_files.cmake | 3 + .../Mac/fpsprofiler_editor_api_files.cmake | 3 + .../Mac/fpsprofiler_private_files.cmake | 8 + .../Mac/fpsprofiler_shared_files.cmake | 8 + .../Code/Platform/Windows/PAL_windows.cmake | 4 + .../Windows/fpsprofiler_api_files.cmake | 3 + .../fpsprofiler_editor_api_files.cmake | 3 + .../Windows/fpsprofiler_private_files.cmake | 8 + .../Windows/fpsprofiler_shared_files.cmake | 8 + .../Code/Platform/iOS/PAL_ios.cmake | 4 + .../Platform/iOS/fpsprofiler_api_files.cmake | 3 + .../iOS/fpsprofiler_private_files.cmake | 8 + .../iOS/fpsprofiler_shared_files.cmake | 8 + .../Code/Source/Clients/FPSProfilerModule.cpp | 21 ++ .../Clients/FPSProfilerSystemComponent.cpp | 152 +++++++++++ .../Clients/FPSProfilerSystemComponent.h | 63 +++++ .../Source/FPSProfilerModuleInterface.cpp | 33 +++ .../Code/Source/FPSProfilerModuleInterface.h | 24 ++ .../Source/Tools/FPSProfilerEditorModule.cpp | 43 +++ .../FPSProfilerEditorSystemComponent.cpp | 101 +++++++ .../Tools/FPSProfilerEditorSystemComponent.h | 41 +++ .../Code/Tests/Clients/FPSProfilerTest.cpp | 4 + .../Tests/Tools/FPSProfilerEditorTest.cpp | 4 + .../Code/fpsprofiler_api_files.cmake | 5 + .../Code/fpsprofiler_editor_api_files.cmake | 4 + .../fpsprofiler_editor_private_files.cmake | 5 + .../fpsprofiler_editor_shared_files.cmake | 4 + .../Code/fpsprofiler_editor_tests_files.cmake | 4 + .../Code/fpsprofiler_private_files.cmake | 7 + .../Code/fpsprofiler_shared_files.cmake | 4 + .../Code/fpsprofiler_tests_files.cmake | 4 + .../Registry/assetprocessor_settings.setreg | 18 ++ Gems/FPSProfiler/gem.json | 28 ++ Gems/FPSProfiler/preview.png | Bin 0 -> 4475 bytes 49 files changed, 1008 insertions(+) create mode 100644 Gems/FPSProfiler/.gitignore create mode 100644 Gems/FPSProfiler/CMakeLists.txt create mode 100644 Gems/FPSProfiler/Code/CMakeLists.txt create mode 100644 Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h create mode 100644 Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h create mode 100644 Gems/FPSProfiler/Code/Platform/Android/PAL_android.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Android/fpsprofiler_api_files.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Android/fpsprofiler_private_files.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Android/fpsprofiler_shared_files.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Linux/PAL_linux.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Linux/fpsprofiler_api_files.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Linux/fpsprofiler_editor_api_files.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Linux/fpsprofiler_private_files.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Linux/fpsprofiler_shared_files.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Mac/PAL_mac.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Mac/fpsprofiler_api_files.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Mac/fpsprofiler_editor_api_files.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Mac/fpsprofiler_private_files.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Mac/fpsprofiler_shared_files.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Windows/PAL_windows.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Windows/fpsprofiler_api_files.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Windows/fpsprofiler_editor_api_files.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Windows/fpsprofiler_private_files.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/Windows/fpsprofiler_shared_files.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/iOS/PAL_ios.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/iOS/fpsprofiler_api_files.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/iOS/fpsprofiler_private_files.cmake create mode 100644 Gems/FPSProfiler/Code/Platform/iOS/fpsprofiler_shared_files.cmake create mode 100644 Gems/FPSProfiler/Code/Source/Clients/FPSProfilerModule.cpp create mode 100644 Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp create mode 100644 Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h create mode 100644 Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp create mode 100644 Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.h create mode 100644 Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp create mode 100644 Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp create mode 100644 Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h create mode 100644 Gems/FPSProfiler/Code/Tests/Clients/FPSProfilerTest.cpp create mode 100644 Gems/FPSProfiler/Code/Tests/Tools/FPSProfilerEditorTest.cpp create mode 100644 Gems/FPSProfiler/Code/fpsprofiler_api_files.cmake create mode 100644 Gems/FPSProfiler/Code/fpsprofiler_editor_api_files.cmake create mode 100644 Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake create mode 100644 Gems/FPSProfiler/Code/fpsprofiler_editor_shared_files.cmake create mode 100644 Gems/FPSProfiler/Code/fpsprofiler_editor_tests_files.cmake create mode 100644 Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake create mode 100644 Gems/FPSProfiler/Code/fpsprofiler_shared_files.cmake create mode 100644 Gems/FPSProfiler/Code/fpsprofiler_tests_files.cmake create mode 100644 Gems/FPSProfiler/Registry/assetprocessor_settings.setreg create mode 100644 Gems/FPSProfiler/gem.json create mode 100644 Gems/FPSProfiler/preview.png diff --git a/Gems/FPSProfiler/.gitignore b/Gems/FPSProfiler/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/Gems/FPSProfiler/CMakeLists.txt b/Gems/FPSProfiler/CMakeLists.txt new file mode 100644 index 00000000..ef8765c6 --- /dev/null +++ b/Gems/FPSProfiler/CMakeLists.txt @@ -0,0 +1,6 @@ + +o3de_gem_setup("FPSProfiler") + +ly_add_external_target_path(${CMAKE_CURRENT_SOURCE_DIR}/3rdParty) + +add_subdirectory(Code) diff --git a/Gems/FPSProfiler/Code/CMakeLists.txt b/Gems/FPSProfiler/Code/CMakeLists.txt new file mode 100644 index 00000000..84258e64 --- /dev/null +++ b/Gems/FPSProfiler/Code/CMakeLists.txt @@ -0,0 +1,257 @@ + +# Currently we are in the Code folder: ${CMAKE_CURRENT_LIST_DIR} +# Get the platform specific folder ${pal_dir} for the current folder: ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME} +# Note: o3de_pal_dir will take care of the details for us, as this may be a restricted platform +# in which case it will see if that platform is present here or in the restricted folder. +# i.e. It could here in our gem : Gems/FPSProfiler/Code/Platform/ or +# //Gems/FPSProfiler/Code +o3de_pal_dir(pal_dir ${CMAKE_CURRENT_LIST_DIR}/Platform/${PAL_PLATFORM_NAME} "${gem_restricted_path}" "${gem_path}" "${gem_parent_relative_path}") + +# Now that we have the platform abstraction layer (PAL) folder for this folder, thats where we will find the +# traits for this platform. Traits for a platform are defines for things like whether or not something in this gem +# is supported by this platform. +include(${pal_dir}/PAL_${PAL_PLATFORM_NAME_LOWERCASE}.cmake) + +# Check to see if building the Gem Modules are supported for the current platform +if(NOT PAL_TRAIT_FPSPROFILER_SUPPORTED) + return() +endif() + +# The ${gem_name}.API target declares the common interface that users of this gem should depend on in their targets +ly_add_target( + NAME ${gem_name}.API INTERFACE + NAMESPACE Gem + FILES_CMAKE + fpsprofiler_api_files.cmake + ${pal_dir}/fpsprofiler_api_files.cmake + INCLUDE_DIRECTORIES + INTERFACE + Include + BUILD_DEPENDENCIES + INTERFACE + AZ::AzCore + Gem::Atom_RPI.Public + Gem::Atom_RPI.Private + Gem::Atom_RPI.Edit + Gem::Atom_RHI.Public + Gem::Atom_RHI.Private + Gem::Atom_RHI.Edit +) + +# The ${gem_name}.Private.Object target is an internal target +# It should not be used outside of this Gems CMakeLists.txt +ly_add_target( + NAME ${gem_name}.Private.Object STATIC + NAMESPACE Gem + FILES_CMAKE + fpsprofiler_private_files.cmake + ${pal_dir}/fpsprofiler_private_files.cmake + TARGET_PROPERTIES + O3DE_PRIVATE_TARGET TRUE + INCLUDE_DIRECTORIES + PRIVATE + Include + Source + BUILD_DEPENDENCIES + PUBLIC + AZ::AzCore + AZ::AzFramework + Gem::Atom_RPI.Public + Gem::Atom_RPI.Private + Gem::Atom_RPI.Edit + Gem::Atom_RHI.Public + Gem::Atom_RHI.Private + Gem::Atom_RHI.Edit +) + +# Here add ${gem_name} target, it depends on the Private Object library and Public API interface +ly_add_target( + NAME ${gem_name} ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE} + NAMESPACE Gem + FILES_CMAKE + fpsprofiler_shared_files.cmake + ${pal_dir}/fpsprofiler_shared_files.cmake + INCLUDE_DIRECTORIES + PUBLIC + Include + PRIVATE + Source + BUILD_DEPENDENCIES + PUBLIC + Gem::${gem_name}.API + PRIVATE + Gem::${gem_name}.Private.Object +) + +# Include the gem name into the Client Module source file +# for use with the AZ_DECLARE_MODULE_CLASS macro +# This is to allow renaming of the gem to also cause +# the CreateModuleClass_Gem_ function which +# is used to bootstrap the gem in monolithic builds to link to the new gem name +ly_add_source_properties( +SOURCES + Source/Clients/FPSProfilerModule.cpp +PROPERTY COMPILE_DEFINITIONS + VALUES + O3DE_GEM_NAME=${gem_name} + O3DE_GEM_VERSION=${gem_version}) + +# By default, we will specify that the above target ${gem_name} would be used by +# Client and Server type targets when this gem is enabled. If you don't want it +# active in Clients or Servers by default, delete one of both of the following lines: +ly_create_alias(NAME ${gem_name}.Clients NAMESPACE Gem TARGETS Gem::${gem_name}) +ly_create_alias(NAME ${gem_name}.Servers NAMESPACE Gem TARGETS Gem::${gem_name}) +ly_create_alias(NAME ${gem_name}.Unified NAMESPACE Gem TARGETS Gem::${gem_name}) + +# For the Client and Server variants of ${gem_name} Gem, an alias to the ${gem_name}.API target will be made +ly_create_alias(NAME ${gem_name}.Clients.API NAMESPACE Gem TARGETS Gem::${gem_name}.API) +ly_create_alias(NAME ${gem_name}.Servers.API NAMESPACE Gem TARGETS Gem::${gem_name}.API) +ly_create_alias(NAME ${gem_name}.Unified.API NAMESPACE Gem TARGETS Gem::${gem_name}.API) + +# Add in CMake dependencies for each gem dependency listed in this gem's gem.json file +# for the Clients, Servers, Unified gem variants +o3de_add_variant_dependencies_for_gem_dependencies(GEM_NAME ${gem_name} VARIANTS Clients Servers Unified) + +# If we are on a host platform, we want to add the host tools targets like the ${gem_name}.Editor MODULE target +if(PAL_TRAIT_BUILD_HOST_TOOLS) + # The ${gem_name}.Editor.API target can be used by other gems that want to interact with the ${gem_name}.Editor module + ly_add_target( + NAME ${gem_name}.Editor.API INTERFACE + NAMESPACE Gem + FILES_CMAKE + fpsprofiler_editor_api_files.cmake + ${pal_dir}/fpsprofiler_editor_api_files.cmake + INCLUDE_DIRECTORIES + INTERFACE + Include + BUILD_DEPENDENCIES + INTERFACE + AZ::AzToolsFramework + ) + + # The ${gem_name}.Editor.Private.Object target is an internal target + # which is only to be used by this gems CMakeLists.txt and any subdirectories + # Other gems should not use this target + ly_add_target( + NAME ${gem_name}.Editor.Private.Object STATIC + NAMESPACE Gem + FILES_CMAKE + fpsprofiler_editor_private_files.cmake + TARGET_PROPERTIES + O3DE_PRIVATE_TARGET TRUE + INCLUDE_DIRECTORIES + PRIVATE + Include + Source + BUILD_DEPENDENCIES + PUBLIC + AZ::AzToolsFramework + ${gem_name}.Private.Object + ) + + ly_add_target( + NAME ${gem_name}.Editor GEM_MODULE + NAMESPACE Gem + AUTOMOC + FILES_CMAKE + fpsprofiler_editor_shared_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Source + PUBLIC + Include + BUILD_DEPENDENCIES + PUBLIC + Gem::${gem_name}.Editor.API + PRIVATE + Gem::${gem_name}.Editor.Private.Object + ) + + # Include the gem name into the Editor Module source file + # for use with the AZ_DECLARE_MODULE_CLASS macro + # This is to allow renaming of the gem to also cause + # the CreateModuleClass_Gem_ function which + # is used to bootstrap the gem in monolithic builds to link to the new gem name + ly_add_source_properties( + SOURCES + Source/Tools/FPSProfilerEditorModule.cpp + PROPERTY COMPILE_DEFINITIONS + VALUES + O3DE_GEM_NAME=${gem_name} + O3DE_GEM_VERSION=${gem_version}) + + # By default, we will specify that the above target ${gem_name} would be used by + # Tool and Builder type targets when this gem is enabled. If you don't want it + # active in Tools or Builders by default, delete one of both of the following lines: + ly_create_alias(NAME ${gem_name}.Tools NAMESPACE Gem TARGETS Gem::${gem_name}.Editor) + ly_create_alias(NAME ${gem_name}.Builders NAMESPACE Gem TARGETS Gem::${gem_name}.Editor) + + # For the Tools and Builders variants of ${gem_name} Gem, an alias to the ${gem_name}.Editor API target will be made + ly_create_alias(NAME ${gem_name}.Tools.API NAMESPACE Gem TARGETS Gem::${gem_name}.Editor.API) + ly_create_alias(NAME ${gem_name}.Builders.API NAMESPACE Gem TARGETS Gem::${gem_name}.Editor.API) + + # Add in CMake dependencies for each gem dependency listed in this gem's gem.json file + # for the Tools and Builders gem variants + o3de_add_variant_dependencies_for_gem_dependencies(GEM_NAME ${gem_name} VARIANTS Tools Builders) +endif() + +################################################################################ +# Tests +################################################################################ +# See if globally, tests are supported +if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) + # We globally support tests, see if we support tests on this platform for ${gem_name}.Tests + if(PAL_TRAIT_FPSPROFILER_TEST_SUPPORTED) + # We support ${gem_name}.Tests on this platform, add dependency on the Private Object target + ly_add_target( + NAME ${gem_name}.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} + NAMESPACE Gem + FILES_CMAKE + fpsprofiler_tests_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Tests + Source + Include + BUILD_DEPENDENCIES + PRIVATE + AZ::AzTest + AZ::AzFramework + Gem::${gem_name}.Private.Object + ) + + # Add ${gem_name}.Tests to googletest + ly_add_googletest( + NAME Gem::${gem_name}.Tests + ) + endif() + + # If we are a host platform we want to add tools test like editor tests here + if(PAL_TRAIT_BUILD_HOST_TOOLS) + # We are a host platform, see if Editor tests are supported on this platform + if(PAL_TRAIT_FPSPROFILER_EDITOR_TEST_SUPPORTED) + # We support ${gem_name}.Editor.Tests on this platform, add ${gem_name}.Editor.Tests target which depends on + # private ${gem_name}.Editor.Private.Object target + ly_add_target( + NAME ${gem_name}.Editor.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} + NAMESPACE Gem + FILES_CMAKE + fpsprofiler_editor_tests_files.cmake + INCLUDE_DIRECTORIES + PRIVATE + Tests + Source + Include + BUILD_DEPENDENCIES + PRIVATE + AZ::AzTest + Gem::${gem_name}.Editor.Private.Object + ) + + # Add ${gem_name}.Editor.Tests to googletest + ly_add_googletest( + NAME Gem::${gem_name}.Editor.Tests + ) + endif() + endif() +endif() diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h new file mode 100644 index 00000000..26912fdd --- /dev/null +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -0,0 +1,33 @@ + +#pragma once + +#include + +#include +#include + +namespace FPSProfiler +{ + class FPSProfilerRequests + { + public: + AZ_RTTI(FPSProfilerRequests, FPSProfilerRequestsTypeId); + virtual ~FPSProfilerRequests() = default; + // Put your public methods here + }; + + class FPSProfilerBusTraits + : public AZ::EBusTraits + { + public: + ////////////////////////////////////////////////////////////////////////// + // EBusTraits overrides + static constexpr AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; + static constexpr AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + ////////////////////////////////////////////////////////////////////////// + }; + + using FPSProfilerRequestBus = AZ::EBus; + using FPSProfilerInterface = AZ::Interface; + +} // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h new file mode 100644 index 00000000..8ad82968 --- /dev/null +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h @@ -0,0 +1,19 @@ + +#pragma once + +namespace FPSProfiler +{ + // System Component TypeIds + inline constexpr const char* FPSProfilerSystemComponentTypeId = "{B11CE88E-E1C5-404F-83D2-0D3850445A13}"; + inline constexpr const char* FPSProfilerEditorSystemComponentTypeId = "{F4308920-CD0B-4A2E-91DE-2EC1E970F97A}"; + + // Module derived classes TypeIds + inline constexpr const char* FPSProfilerModuleInterfaceTypeId = "{77EF155C-6E75-41B1-A939-AF5E2FE4FC6B}"; + inline constexpr const char* FPSProfilerModuleTypeId = "{2A84347B-7657-4BE1-BEA6-246ADB4F04FB}"; + // The Editor Module by default is mutually exclusive with the Client Module + // so they use the Same TypeId + inline constexpr const char* FPSProfilerEditorModuleTypeId = FPSProfilerModuleTypeId; + + // Interface TypeIds + inline constexpr const char* FPSProfilerRequestsTypeId = "{D585EA71-B052-4C97-8647-4B3511CC7C5B}"; +} // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Platform/Android/PAL_android.cmake b/Gems/FPSProfiler/Code/Platform/Android/PAL_android.cmake new file mode 100644 index 00000000..dda583fa --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Android/PAL_android.cmake @@ -0,0 +1,4 @@ + +set(PAL_TRAIT_FPSPROFILER_SUPPORTED TRUE) +set(PAL_TRAIT_FPSPROFILER_TEST_SUPPORTED FALSE) +set(PAL_TRAIT_FPSPROFILER_EDITOR_TEST_SUPPORTED FALSE) diff --git a/Gems/FPSProfiler/Code/Platform/Android/fpsprofiler_api_files.cmake b/Gems/FPSProfiler/Code/Platform/Android/fpsprofiler_api_files.cmake new file mode 100644 index 00000000..f5526eeb --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Android/fpsprofiler_api_files.cmake @@ -0,0 +1,3 @@ + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Platform/Android/fpsprofiler_private_files.cmake b/Gems/FPSProfiler/Code/Platform/Android/fpsprofiler_private_files.cmake new file mode 100644 index 00000000..4b8014c2 --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Android/fpsprofiler_private_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Android +# i.e. ../Source/Android/FPSProfilerAndroid.cpp +# ../Source/Android/FPSProfilerAndroid.h +# ../Include/Android/FPSProfilerAndroid.h + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Platform/Android/fpsprofiler_shared_files.cmake b/Gems/FPSProfiler/Code/Platform/Android/fpsprofiler_shared_files.cmake new file mode 100644 index 00000000..4b8014c2 --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Android/fpsprofiler_shared_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Android +# i.e. ../Source/Android/FPSProfilerAndroid.cpp +# ../Source/Android/FPSProfilerAndroid.h +# ../Include/Android/FPSProfilerAndroid.h + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Platform/Linux/PAL_linux.cmake b/Gems/FPSProfiler/Code/Platform/Linux/PAL_linux.cmake new file mode 100644 index 00000000..dda583fa --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Linux/PAL_linux.cmake @@ -0,0 +1,4 @@ + +set(PAL_TRAIT_FPSPROFILER_SUPPORTED TRUE) +set(PAL_TRAIT_FPSPROFILER_TEST_SUPPORTED FALSE) +set(PAL_TRAIT_FPSPROFILER_EDITOR_TEST_SUPPORTED FALSE) diff --git a/Gems/FPSProfiler/Code/Platform/Linux/fpsprofiler_api_files.cmake b/Gems/FPSProfiler/Code/Platform/Linux/fpsprofiler_api_files.cmake new file mode 100644 index 00000000..f5526eeb --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Linux/fpsprofiler_api_files.cmake @@ -0,0 +1,3 @@ + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Platform/Linux/fpsprofiler_editor_api_files.cmake b/Gems/FPSProfiler/Code/Platform/Linux/fpsprofiler_editor_api_files.cmake new file mode 100644 index 00000000..f5526eeb --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Linux/fpsprofiler_editor_api_files.cmake @@ -0,0 +1,3 @@ + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Platform/Linux/fpsprofiler_private_files.cmake b/Gems/FPSProfiler/Code/Platform/Linux/fpsprofiler_private_files.cmake new file mode 100644 index 00000000..6b14fff1 --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Linux/fpsprofiler_private_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Linux +# i.e. ../Source/Linux/FPSProfilerLinux.cpp +# ../Source/Linux/FPSProfilerLinux.h +# ../Include/Linux/FPSProfilerLinux.h + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Platform/Linux/fpsprofiler_shared_files.cmake b/Gems/FPSProfiler/Code/Platform/Linux/fpsprofiler_shared_files.cmake new file mode 100644 index 00000000..6b14fff1 --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Linux/fpsprofiler_shared_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Linux +# i.e. ../Source/Linux/FPSProfilerLinux.cpp +# ../Source/Linux/FPSProfilerLinux.h +# ../Include/Linux/FPSProfilerLinux.h + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Platform/Mac/PAL_mac.cmake b/Gems/FPSProfiler/Code/Platform/Mac/PAL_mac.cmake new file mode 100644 index 00000000..dda583fa --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Mac/PAL_mac.cmake @@ -0,0 +1,4 @@ + +set(PAL_TRAIT_FPSPROFILER_SUPPORTED TRUE) +set(PAL_TRAIT_FPSPROFILER_TEST_SUPPORTED FALSE) +set(PAL_TRAIT_FPSPROFILER_EDITOR_TEST_SUPPORTED FALSE) diff --git a/Gems/FPSProfiler/Code/Platform/Mac/fpsprofiler_api_files.cmake b/Gems/FPSProfiler/Code/Platform/Mac/fpsprofiler_api_files.cmake new file mode 100644 index 00000000..f5526eeb --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Mac/fpsprofiler_api_files.cmake @@ -0,0 +1,3 @@ + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Platform/Mac/fpsprofiler_editor_api_files.cmake b/Gems/FPSProfiler/Code/Platform/Mac/fpsprofiler_editor_api_files.cmake new file mode 100644 index 00000000..f5526eeb --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Mac/fpsprofiler_editor_api_files.cmake @@ -0,0 +1,3 @@ + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Platform/Mac/fpsprofiler_private_files.cmake b/Gems/FPSProfiler/Code/Platform/Mac/fpsprofiler_private_files.cmake new file mode 100644 index 00000000..0ed95358 --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Mac/fpsprofiler_private_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Mac +# i.e. ../Source/Mac/FPSProfilerMac.cpp +# ../Source/Mac/FPSProfilerMac.h +# ../Include/Mac/FPSProfilerMac.h + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Platform/Mac/fpsprofiler_shared_files.cmake b/Gems/FPSProfiler/Code/Platform/Mac/fpsprofiler_shared_files.cmake new file mode 100644 index 00000000..0ed95358 --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Mac/fpsprofiler_shared_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Mac +# i.e. ../Source/Mac/FPSProfilerMac.cpp +# ../Source/Mac/FPSProfilerMac.h +# ../Include/Mac/FPSProfilerMac.h + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Platform/Windows/PAL_windows.cmake b/Gems/FPSProfiler/Code/Platform/Windows/PAL_windows.cmake new file mode 100644 index 00000000..dda583fa --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Windows/PAL_windows.cmake @@ -0,0 +1,4 @@ + +set(PAL_TRAIT_FPSPROFILER_SUPPORTED TRUE) +set(PAL_TRAIT_FPSPROFILER_TEST_SUPPORTED FALSE) +set(PAL_TRAIT_FPSPROFILER_EDITOR_TEST_SUPPORTED FALSE) diff --git a/Gems/FPSProfiler/Code/Platform/Windows/fpsprofiler_api_files.cmake b/Gems/FPSProfiler/Code/Platform/Windows/fpsprofiler_api_files.cmake new file mode 100644 index 00000000..f5526eeb --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Windows/fpsprofiler_api_files.cmake @@ -0,0 +1,3 @@ + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Platform/Windows/fpsprofiler_editor_api_files.cmake b/Gems/FPSProfiler/Code/Platform/Windows/fpsprofiler_editor_api_files.cmake new file mode 100644 index 00000000..f5526eeb --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Windows/fpsprofiler_editor_api_files.cmake @@ -0,0 +1,3 @@ + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Platform/Windows/fpsprofiler_private_files.cmake b/Gems/FPSProfiler/Code/Platform/Windows/fpsprofiler_private_files.cmake new file mode 100644 index 00000000..5c663edc --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Windows/fpsprofiler_private_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Windows +# i.e. ../Source/Windows/FPSProfilerWindows.cpp +# ../Source/Windows/FPSProfilerWindows.h +# ../Include/Windows/FPSProfilerWindows.h + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Platform/Windows/fpsprofiler_shared_files.cmake b/Gems/FPSProfiler/Code/Platform/Windows/fpsprofiler_shared_files.cmake new file mode 100644 index 00000000..5c663edc --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/Windows/fpsprofiler_shared_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for Windows +# i.e. ../Source/Windows/FPSProfilerWindows.cpp +# ../Source/Windows/FPSProfilerWindows.h +# ../Include/Windows/FPSProfilerWindows.h + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Platform/iOS/PAL_ios.cmake b/Gems/FPSProfiler/Code/Platform/iOS/PAL_ios.cmake new file mode 100644 index 00000000..dda583fa --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/iOS/PAL_ios.cmake @@ -0,0 +1,4 @@ + +set(PAL_TRAIT_FPSPROFILER_SUPPORTED TRUE) +set(PAL_TRAIT_FPSPROFILER_TEST_SUPPORTED FALSE) +set(PAL_TRAIT_FPSPROFILER_EDITOR_TEST_SUPPORTED FALSE) diff --git a/Gems/FPSProfiler/Code/Platform/iOS/fpsprofiler_api_files.cmake b/Gems/FPSProfiler/Code/Platform/iOS/fpsprofiler_api_files.cmake new file mode 100644 index 00000000..f5526eeb --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/iOS/fpsprofiler_api_files.cmake @@ -0,0 +1,3 @@ + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Platform/iOS/fpsprofiler_private_files.cmake b/Gems/FPSProfiler/Code/Platform/iOS/fpsprofiler_private_files.cmake new file mode 100644 index 00000000..16311cd5 --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/iOS/fpsprofiler_private_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for iOS +# i.e. ../Source/iOS/FPSProfileriOS.cpp +# ../Source/iOS/FPSProfileriOS.h +# ../Include/iOS/FPSProfileriOS.h + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Platform/iOS/fpsprofiler_shared_files.cmake b/Gems/FPSProfiler/Code/Platform/iOS/fpsprofiler_shared_files.cmake new file mode 100644 index 00000000..16311cd5 --- /dev/null +++ b/Gems/FPSProfiler/Code/Platform/iOS/fpsprofiler_shared_files.cmake @@ -0,0 +1,8 @@ + +# Platform specific files for iOS +# i.e. ../Source/iOS/FPSProfileriOS.cpp +# ../Source/iOS/FPSProfileriOS.h +# ../Include/iOS/FPSProfileriOS.h + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerModule.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerModule.cpp new file mode 100644 index 00000000..0516b177 --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerModule.cpp @@ -0,0 +1,21 @@ + +#include +#include +#include "FPSProfilerSystemComponent.h" + +namespace FPSProfiler +{ + class FPSProfilerModule + : public FPSProfilerModuleInterface + { + public: + AZ_RTTI(FPSProfilerModule, FPSProfilerModuleTypeId, FPSProfilerModuleInterface); + AZ_CLASS_ALLOCATOR(FPSProfilerModule, AZ::SystemAllocator); + }; +}// namespace FPSProfiler + +#if defined(O3DE_GEM_NAME) +AZ_DECLARE_MODULE_CLASS(AZ_JOIN(Gem_, O3DE_GEM_NAME), FPSProfiler::FPSProfilerModule) +#else +AZ_DECLARE_MODULE_CLASS(Gem_FPSProfiler, FPSProfiler::FPSProfilerModule) +#endif diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp new file mode 100644 index 00000000..13b68fea --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -0,0 +1,152 @@ + +#include "FPSProfilerSystemComponent.h" + +#include "Atom/RHI/RHISystemInterface.h" +#include "Atom/RPI.Public/RPISystemInterface.h" +#include "AzCore/IO/FileIO.h" + +#include + +#include + +namespace FPSProfiler +{ + AZ_COMPONENT_IMPL(FPSProfilerSystemComponent, "FPSProfilerSystemComponent", + FPSProfilerSystemComponentTypeId); + + void FPSProfilerSystemComponent::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ; + } + } + + void FPSProfilerSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("FPSProfilerService")); + } + + void FPSProfilerSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC_CE("FPSProfilerService")); + } + + void FPSProfilerSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) + { + } + + void FPSProfilerSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + } + + FPSProfilerSystemComponent::FPSProfilerSystemComponent() + { + if (FPSProfilerInterface::Get() == nullptr) + { + FPSProfilerInterface::Register(this); + } + } + + FPSProfilerSystemComponent::FPSProfilerSystemComponent(const AZ::IO::Path& m_outputFilename, const bool& m_SaveMultiple) + : m_outputFilename(m_outputFilename), m_SaveMultiple(m_SaveMultiple) + { + } + + FPSProfilerSystemComponent::~FPSProfilerSystemComponent() + { + if (FPSProfilerInterface::Get() == this) + { + FPSProfilerInterface::Unregister(this); + } + } + + void FPSProfilerSystemComponent::Init() + { + } + + void FPSProfilerSystemComponent::Activate() + { + FPSProfilerRequestBus::Handler::BusConnect(); + AZ::TickBus::Handler::BusConnect(); + + if (m_outputFilename.empty()) + { + m_outputFilename = "@user@/fps_log.csv"; // Default location in user log + } + + AZ::IO::FileIOStream file(m_outputFilename.c_str(), AZ::IO::OpenMode::ModeWrite); + AZStd::string csvHeader = "Frame,FrameTime,InstantFPS,MinFPS,MaxFPS,AvgFPS,GpuMemoryUsed\n"; + file.Write(csvHeader.size(), csvHeader.c_str()); + file.Close(); + + AZ_Printf("FPS Profiler", "FPS Profiler Activated on Level."); + } + + void FPSProfilerSystemComponent::Deactivate() + { + AZ::TickBus::Handler::BusDisconnect(); + WriteDataToFile(); + + FPSProfilerRequestBus::Handler::BusDisconnect(); + } + + void FPSProfilerSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) + { + float fps = deltaTime > 0 ? (1.0f / deltaTime) : 0.0f; + + m_fpsSamples.push_back(fps); + m_minFPS = AZStd::min(m_minFPS, fps); + m_maxFPS = AZStd::max(m_maxFPS, fps); + m_totalFrameTime += deltaTime; + m_frameCount++; + + float avgFPS = (m_frameCount > 0) ? (m_frameCount / m_totalFrameTime) : 0.0f; + + float gpuMemoryUsed = 0.0f; + if (AZ::RPI::RPISystemInterface* rpiSystem = AZ::RPI::RPISystemInterface::Get()) + { + // gpuMemoryUsed = static_cast(rpiSystem->GetCurrentCpuMemoryUsage()); + [[maybe_unused]]AZ::RHI::RHISystemInterface* rhiSystem = AZ::RHI::RHISystemInterface::Get(); + AZ::RHI::MemoryStatistics memoryStatistics; + } + + AZStd::string logEntry = AZStd::string::format( + "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f\n", + m_frameCount, deltaTime, fps, m_minFPS, m_maxFPS, avgFPS, gpuMemoryUsed + ); + m_logEntries.push_back(logEntry); + + if (m_frameCount % 100 == 0) + { + WriteDataToFile(); + } + } + + int FPSProfilerSystemComponent::GetTickOrder() + { + return AZ::TICK_GAME; + } + + void FPSProfilerSystemComponent::WriteDataToFile() + { + if (!m_logEntries.empty()) + { + for (int i = 0; i < sizeof(uint8_t); ++i) + { + + } + + AZ::IO::FileIOStream file(m_outputFilename.c_str(), AZ::IO::OpenMode::ModeAppend | AZ::IO::OpenMode::ModeWrite); + + for (const auto& entry : m_logEntries) + { + file.Write(entry.size(), entry.c_str()); + } + file.Close(); + m_logEntries.clear(); + } + } +} // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h new file mode 100644 index 00000000..c3b87816 --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -0,0 +1,63 @@ + +#pragma once + +#include +#include +#include +#include + +namespace FPSProfiler +{ + class FPSProfilerSystemComponent + : public AZ::Component + , protected FPSProfilerRequestBus::Handler + , public AZ::TickBus::Handler + { + public: + AZ_COMPONENT_DECL(FPSProfilerSystemComponent); + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + FPSProfilerSystemComponent(); + FPSProfilerSystemComponent(const AZ::IO::Path& m_outputFilename, const bool& m_SaveMultiple); + ~FPSProfilerSystemComponent(); + + protected: + //////////////////////////////////////////////////////////////////////// + // FPSProfilerRequestBus interface implementation + + //////////////////////////////////////////////////////////////////////// + + //////////////////////////////////////////////////////////////////////// + // AZ::Component interface implementation + void Init() override; + void Activate() override; + void Deactivate() override; + //////////////////////////////////////////////////////////////////////// + + //////////////////////////////////////////////////////////////////////// + // AZTickBus interface implementation + void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; + int GetTickOrder() override; + //////////////////////////////////////////////////////////////////////// + private: + AZStd::vector m_fpsSamples; + AZStd::vector m_logEntries; + + float m_minFPS = FLT_MAX; // Tracking the lowest FPS value - set to max for difference + float m_maxFPS = 0.0f; // Tracking the highest FPS value - set to min for diff + float m_totalFrameTime = 0.0f; + int m_frameCount = 0; + + AZ::IO::Path m_outputFilename; + bool m_SaveMultiple; + + void WriteDataToFile(); + }; + +} // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp new file mode 100644 index 00000000..b40677f2 --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp @@ -0,0 +1,33 @@ + +#include "FPSProfilerModuleInterface.h" +#include + +#include + +#include + +namespace FPSProfiler +{ + AZ_TYPE_INFO_WITH_NAME_IMPL(FPSProfilerModuleInterface, + "FPSProfilerModuleInterface", FPSProfilerModuleInterfaceTypeId); + AZ_RTTI_NO_TYPE_INFO_IMPL(FPSProfilerModuleInterface, AZ::Module); + AZ_CLASS_ALLOCATOR_IMPL(FPSProfilerModuleInterface, AZ::SystemAllocator); + + FPSProfilerModuleInterface::FPSProfilerModuleInterface() + { + // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here. + // Add ALL components descriptors associated with this gem to m_descriptors. + // This will associate the AzTypeInfo information for the components with the the SerializeContext, BehaviorContext and EditContext. + // This happens through the [MyComponent]::Reflect() function. + m_descriptors.insert(m_descriptors.end(), { + FPSProfilerSystemComponent::CreateDescriptor(), + }); + } + + AZ::ComponentTypeList FPSProfilerModuleInterface::GetRequiredSystemComponents() const + { + return AZ::ComponentTypeList{ + azrtti_typeid(), + }; + } +} // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.h b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.h new file mode 100644 index 00000000..c2737c5e --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.h @@ -0,0 +1,24 @@ + +#include +#include +#include +#include + +namespace FPSProfiler +{ + class FPSProfilerModuleInterface + : public AZ::Module + { + public: + AZ_TYPE_INFO_WITH_NAME_DECL(FPSProfilerModuleInterface) + AZ_RTTI_NO_TYPE_INFO_DECL() + AZ_CLASS_ALLOCATOR_DECL + + FPSProfilerModuleInterface(); + + /** + * Add required SystemComponents to the SystemEntity. + */ + AZ::ComponentTypeList GetRequiredSystemComponents() const override; + }; +}// namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp new file mode 100644 index 00000000..7d8d546c --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp @@ -0,0 +1,43 @@ + +#include +#include +#include "FPSProfilerEditorSystemComponent.h" + +namespace FPSProfiler +{ + class FPSProfilerEditorModule + : public FPSProfilerModuleInterface + { + public: + AZ_RTTI(FPSProfilerEditorModule, FPSProfilerEditorModuleTypeId, FPSProfilerModuleInterface); + AZ_CLASS_ALLOCATOR(FPSProfilerEditorModule, AZ::SystemAllocator); + + FPSProfilerEditorModule() + { + // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here. + // Add ALL components descriptors associated with this gem to m_descriptors. + // This will associate the AzTypeInfo information for the components with the the SerializeContext, BehaviorContext and EditContext. + // This happens through the [MyComponent]::Reflect() function. + m_descriptors.insert(m_descriptors.end(), { + FPSProfilerEditorSystemComponent::CreateDescriptor(), + }); + } + + /** + * Add required SystemComponents to the SystemEntity. + * Non-SystemComponents should not be added here + */ + AZ::ComponentTypeList GetRequiredSystemComponents() const override + { + return AZ::ComponentTypeList { + azrtti_typeid(), + }; + } + }; +}// namespace FPSProfiler + +#if defined(O3DE_GEM_NAME) +AZ_DECLARE_MODULE_CLASS(AZ_JOIN(Gem_, O3DE_GEM_NAME, _Editor), FPSProfiler::FPSProfilerEditorModule) +#else +AZ_DECLARE_MODULE_CLASS(Gem_FPSProfiler_Editor, FPSProfiler::FPSProfilerEditorModule) +#endif diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp new file mode 100644 index 00000000..b84b36cb --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -0,0 +1,101 @@ + +#include "FPSProfilerEditorSystemComponent.h" + +#include "AzQtComponents/Components/Widgets/FileDialog.h" +#include "UI/UICore/WidgetHelpers.h" + +#include + +#include + +namespace FPSProfiler +{ + AZ_COMPONENT_IMPL(FPSProfilerEditorSystemComponent, "FPSProfilerEditorSystemComponent", + FPSProfilerEditorSystemComponentTypeId, BaseSystemComponent); + + void FPSProfilerEditorSystemComponent::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ->Field("m_OutputFilename", &FPSProfilerEditorSystemComponent::m_OutputFilename) + ->Field("m_SaveMultiple", &FPSProfilerEditorSystemComponent::m_SaveMultiple) + ->Field("m_SaveFPSData", &FPSProfilerEditorSystemComponent::m_SaveFPSData) + ->Field("m_SaveGPUData", &FPSProfilerEditorSystemComponent::m_SaveGPUData) + ->Field("m_SaveCPUData", &FPSProfilerEditorSystemComponent::m_SaveCPUData) + ->Field("m_ShowFPS", &FPSProfilerEditorSystemComponent::m_ShowFPS); + + + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class("FPS Profiler", "Tracks FPS, GPU and CPU performance and saves it into .csv") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Category, "Performance") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) + ->Attribute(AZ::Edit::Attributes::AutoExpand, false) + + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_OutputFilename, "Csv Save Path", "Select a path where *.csv will be saved.") + ->Attribute(AZ::Edit::Attributes::SourceAssetFilterPattern, "*.csv") + + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_SaveMultiple, "Save Multiple", + "When enabled, system will save files without overwriting current file. Each file will have prefix *_n.csv postfix numeration.") + + ->ClassElement(AZ::Edit::ClassElements::Group, "Data Settings") + + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_SaveFPSData, "Save FPS Data", + "When enabled, system will collect FPS data into csv.") + + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_SaveGPUData, "Save GPU Data", + "When enabled, system will collect GPU usage data into csv.") + + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_SaveCPUData, "Save CPU Data", + "When enabled, system will collect CPU usage data into csv.") + + ->ClassElement(AZ::Edit::ClassElements::Group, "Debug Settings") + + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_ShowFPS, "Show FPS", + "When enabled, system will show FPS counter in top-left corner.") + ; + } + } + } + + FPSProfilerEditorSystemComponent::FPSProfilerEditorSystemComponent() = default; + + FPSProfilerEditorSystemComponent::~FPSProfilerEditorSystemComponent() = default; + + void FPSProfilerEditorSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + BaseSystemComponent::GetProvidedServices(provided); + provided.push_back(AZ_CRC_CE("FPSProfilerEditorService")); + } + + void FPSProfilerEditorSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + BaseSystemComponent::GetIncompatibleServices(incompatible); + incompatible.push_back(AZ_CRC_CE("FPSProfilerEditorService")); + } + + void FPSProfilerEditorSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) + { + BaseSystemComponent::GetRequiredServices(required); + } + + void FPSProfilerEditorSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent) + { + BaseSystemComponent::GetDependentServices(dependent); + } + + void FPSProfilerEditorSystemComponent::Activate() + { + FPSProfilerSystemComponent::Activate(); + AzToolsFramework::EditorEvents::Bus::Handler::BusConnect(); + } + + void FPSProfilerEditorSystemComponent::Deactivate() + { + AzToolsFramework::EditorEvents::Bus::Handler::BusDisconnect(); + FPSProfilerSystemComponent::Deactivate(); + } +} // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h new file mode 100644 index 00000000..0ee0d9a0 --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h @@ -0,0 +1,41 @@ + +#pragma once + +#include + +#include + +namespace FPSProfiler +{ + /// System component for FPSProfiler editor + class FPSProfilerEditorSystemComponent + : public FPSProfilerSystemComponent + , protected AzToolsFramework::EditorEvents::Bus::Handler + { + using BaseSystemComponent = FPSProfilerSystemComponent; + public: + AZ_COMPONENT_DECL(FPSProfilerEditorSystemComponent); + + static void Reflect(AZ::ReflectContext* context); + + FPSProfilerEditorSystemComponent(); + ~FPSProfilerEditorSystemComponent(); + + private: + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); + + // AZ::Component + void Activate() override; + void Deactivate() override; + + AZ::IO::Path m_OutputFilename = "@user@/fps_log.csv"; + bool m_SaveFPSData = true; + bool m_SaveMultiple = true; + bool m_SaveGPUData = true; + bool m_SaveCPUData = true; + bool m_ShowFPS = true; + }; +} // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Tests/Clients/FPSProfilerTest.cpp b/Gems/FPSProfiler/Code/Tests/Clients/FPSProfilerTest.cpp new file mode 100644 index 00000000..274a9908 --- /dev/null +++ b/Gems/FPSProfiler/Code/Tests/Clients/FPSProfilerTest.cpp @@ -0,0 +1,4 @@ + +#include + +AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV); diff --git a/Gems/FPSProfiler/Code/Tests/Tools/FPSProfilerEditorTest.cpp b/Gems/FPSProfiler/Code/Tests/Tools/FPSProfilerEditorTest.cpp new file mode 100644 index 00000000..274a9908 --- /dev/null +++ b/Gems/FPSProfiler/Code/Tests/Tools/FPSProfilerEditorTest.cpp @@ -0,0 +1,4 @@ + +#include + +AZ_UNIT_TEST_HOOK(DEFAULT_UNIT_TEST_ENV); diff --git a/Gems/FPSProfiler/Code/fpsprofiler_api_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_api_files.cmake new file mode 100644 index 00000000..5f4a8c26 --- /dev/null +++ b/Gems/FPSProfiler/Code/fpsprofiler_api_files.cmake @@ -0,0 +1,5 @@ + +set(FILES + Include/FPSProfiler/FPSProfilerBus.h + Include/FPSProfiler/FPSProfilerTypeIds.h +) diff --git a/Gems/FPSProfiler/Code/fpsprofiler_editor_api_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_editor_api_files.cmake new file mode 100644 index 00000000..8cd37a5c --- /dev/null +++ b/Gems/FPSProfiler/Code/fpsprofiler_editor_api_files.cmake @@ -0,0 +1,4 @@ + + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake new file mode 100644 index 00000000..934da04a --- /dev/null +++ b/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake @@ -0,0 +1,5 @@ + +set(FILES + Source/Tools/FPSProfilerEditorSystemComponent.cpp + Source/Tools/FPSProfilerEditorSystemComponent.h +) diff --git a/Gems/FPSProfiler/Code/fpsprofiler_editor_shared_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_editor_shared_files.cmake new file mode 100644 index 00000000..af1d259a --- /dev/null +++ b/Gems/FPSProfiler/Code/fpsprofiler_editor_shared_files.cmake @@ -0,0 +1,4 @@ + +set(FILES + Source/Tools/FPSProfilerEditorModule.cpp +) diff --git a/Gems/FPSProfiler/Code/fpsprofiler_editor_tests_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_editor_tests_files.cmake new file mode 100644 index 00000000..341daf50 --- /dev/null +++ b/Gems/FPSProfiler/Code/fpsprofiler_editor_tests_files.cmake @@ -0,0 +1,4 @@ + +set(FILES + Tests/Tools/FPSProfilerEditorTest.cpp +) diff --git a/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake new file mode 100644 index 00000000..92006e5d --- /dev/null +++ b/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake @@ -0,0 +1,7 @@ + +set(FILES + Source/FPSProfilerModuleInterface.cpp + Source/FPSProfilerModuleInterface.h + Source/Clients/FPSProfilerSystemComponent.cpp + Source/Clients/FPSProfilerSystemComponent.h +) diff --git a/Gems/FPSProfiler/Code/fpsprofiler_shared_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_shared_files.cmake new file mode 100644 index 00000000..c88915d8 --- /dev/null +++ b/Gems/FPSProfiler/Code/fpsprofiler_shared_files.cmake @@ -0,0 +1,4 @@ + +set(FILES + Source/Clients/FPSProfilerModule.cpp +) diff --git a/Gems/FPSProfiler/Code/fpsprofiler_tests_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_tests_files.cmake new file mode 100644 index 00000000..2d2f12fd --- /dev/null +++ b/Gems/FPSProfiler/Code/fpsprofiler_tests_files.cmake @@ -0,0 +1,4 @@ + +set(FILES + Tests/Clients/FPSProfilerTest.cpp +) diff --git a/Gems/FPSProfiler/Registry/assetprocessor_settings.setreg b/Gems/FPSProfiler/Registry/assetprocessor_settings.setreg new file mode 100644 index 00000000..86b96ccd --- /dev/null +++ b/Gems/FPSProfiler/Registry/assetprocessor_settings.setreg @@ -0,0 +1,18 @@ +{ + "Amazon": { + "AssetProcessor": { + "Settings": { + "ScanFolder FPSProfiler/Assets": { + "watch": "@GEMROOT:FPSProfiler@/Assets", + "recursive": 1, + "order": 101 + }, + "ScanFolder FPSProfiler/Registry": { + "watch": "@GEMROOT:FPSProfiler@/Registry", + "recursive": 1, + "order": 102 + } + } + } + } +} diff --git a/Gems/FPSProfiler/gem.json b/Gems/FPSProfiler/gem.json new file mode 100644 index 00000000..c9755cc3 --- /dev/null +++ b/Gems/FPSProfiler/gem.json @@ -0,0 +1,28 @@ +{ + "gem_name": "FPSProfiler", + "version": "1.0.0", + "display_name": "FPSProfiler", + "license": "License used i.e. Apache-2.0 or MIT", + "license_url": "Link to the license web site i.e. https://opensource.org/licenses/Apache-2.0", + "origin": "The name of the originator or creator", + "origin_url": "The website for this Gem", + "type": "Code", + "summary": "A short description of this Gem", + "canonical_tags": [ + "Gem" + ], + "user_tags": [ + "FPSProfiler" + ], + "platforms": [ + "" + ], + "icon_path": "preview.png", + "requirements": "Notice of any requirements for this Gem i.e. This requires X other gem", + "documentation_url": "Link to any documentation of your Gem", + "dependencies": [], + "repo_uri": "", + "compatible_engines": [], + "engine_api_dependencies": [], + "restricted": "FPSProfiler" +} diff --git a/Gems/FPSProfiler/preview.png b/Gems/FPSProfiler/preview.png new file mode 100644 index 0000000000000000000000000000000000000000..83afae48b97f808914d4b210c578441b7cdfdcdd GIT binary patch literal 4475 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 Date: Mon, 24 Feb 2025 10:28:28 +0100 Subject: [PATCH 002/175] Add profiler configuration Signed-off-by: Wojciech Czerski --- .../Include/FPSProfiler/FPSProfilerTypeIds.h | 2 + .../Clients/FPSProfilerSystemComponent.cpp | 12 ++-- .../Clients/FPSProfilerSystemComponent.h | 7 +- .../Code/Source/Tools/FPSProfilerData.cpp | 53 +++++++++++++++ .../Code/Source/Tools/FPSProfilerData.h | 22 +++++++ .../FPSProfilerEditorSystemComponent.cpp | 66 ++++--------------- .../Tools/FPSProfilerEditorSystemComponent.h | 29 ++++---- .../fpsprofiler_editor_private_files.cmake | 2 + 8 files changed, 113 insertions(+), 80 deletions(-) create mode 100644 Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp create mode 100644 Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h index 8ad82968..a56ce0ab 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h @@ -6,10 +6,12 @@ namespace FPSProfiler // System Component TypeIds inline constexpr const char* FPSProfilerSystemComponentTypeId = "{B11CE88E-E1C5-404F-83D2-0D3850445A13}"; inline constexpr const char* FPSProfilerEditorSystemComponentTypeId = "{F4308920-CD0B-4A2E-91DE-2EC1E970F97A}"; + inline constexpr const char* FPSProfilerDataTypeId = "{70857242-4363-403C-ACF1-4A401B1024B5}"; // Module derived classes TypeIds inline constexpr const char* FPSProfilerModuleInterfaceTypeId = "{77EF155C-6E75-41B1-A939-AF5E2FE4FC6B}"; inline constexpr const char* FPSProfilerModuleTypeId = "{2A84347B-7657-4BE1-BEA6-246ADB4F04FB}"; + // The Editor Module by default is mutually exclusive with the Client Module // so they use the Same TypeId inline constexpr const char* FPSProfilerEditorModuleTypeId = FPSProfilerModuleTypeId; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 13b68fea..77a71281 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -50,8 +50,8 @@ namespace FPSProfiler } } - FPSProfilerSystemComponent::FPSProfilerSystemComponent(const AZ::IO::Path& m_outputFilename, const bool& m_SaveMultiple) - : m_outputFilename(m_outputFilename), m_SaveMultiple(m_SaveMultiple) + FPSProfilerSystemComponent::FPSProfilerSystemComponent(const FPSProfilerData& m_Configuration) + : m_ProfilerData(m_Configuration) { } @@ -72,12 +72,12 @@ namespace FPSProfiler FPSProfilerRequestBus::Handler::BusConnect(); AZ::TickBus::Handler::BusConnect(); - if (m_outputFilename.empty()) + if (m_ProfilerData.m_OutputFilename.empty()) { - m_outputFilename = "@user@/fps_log.csv"; // Default location in user log + m_ProfilerData.m_OutputFilename = "@user@/fps_log.csv"; // Default location in user log } - AZ::IO::FileIOStream file(m_outputFilename.c_str(), AZ::IO::OpenMode::ModeWrite); + AZ::IO::FileIOStream file(m_ProfilerData.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite); AZStd::string csvHeader = "Frame,FrameTime,InstantFPS,MinFPS,MaxFPS,AvgFPS,GpuMemoryUsed\n"; file.Write(csvHeader.size(), csvHeader.c_str()); file.Close(); @@ -139,7 +139,7 @@ namespace FPSProfiler } - AZ::IO::FileIOStream file(m_outputFilename.c_str(), AZ::IO::OpenMode::ModeAppend | AZ::IO::OpenMode::ModeWrite); + AZ::IO::FileIOStream file(m_ProfilerData.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeAppend | AZ::IO::OpenMode::ModeWrite); for (const auto& entry : m_logEntries) { diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index c3b87816..ca99798a 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -1,6 +1,8 @@ #pragma once +#include + #include #include #include @@ -24,7 +26,7 @@ namespace FPSProfiler static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); FPSProfilerSystemComponent(); - FPSProfilerSystemComponent(const AZ::IO::Path& m_outputFilename, const bool& m_SaveMultiple); + FPSProfilerSystemComponent(const FPSProfilerData& m_Configuration); ~FPSProfilerSystemComponent(); protected: @@ -54,8 +56,7 @@ namespace FPSProfiler float m_totalFrameTime = 0.0f; int m_frameCount = 0; - AZ::IO::Path m_outputFilename; - bool m_SaveMultiple; + FPSProfilerData m_ProfilerData; void WriteDataToFile(); }; diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp new file mode 100644 index 00000000..0c819fee --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp @@ -0,0 +1,53 @@ + +#include "FPSProfilerData.h" + +#include + +namespace FPSProfiler +{ + void FPSProfilerData::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ->Field("m_OutputFilename", &FPSProfilerData::m_OutputFilename) + ->Field("m_SaveMultiple", &FPSProfilerData::m_SaveMultiple) + ->Field("m_SaveFPSData", &FPSProfilerData::m_SaveFPSData) + ->Field("m_SaveCPUData", &FPSProfilerData::m_SaveCPUData) + ->Field("m_SaveGPUData", &FPSProfilerData::m_SaveGPUData) + ->Field("m_ShowFPS", &FPSProfilerData::m_ShowFPS); + + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class("FPS Profiler Configuration", "Tracks FPS, GPU and CPU performance and saves it into .csv") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Category, "Performance") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerData::m_OutputFilename, "Csv Save Path", "Select a path where *.csv will be saved.") + ->Attribute(AZ::Edit::Attributes::SourceAssetFilterPattern, "*.csv") + + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerData::m_SaveMultiple, "Save Multiple", + "When enabled, system will save files without overwriting current file. Each file will have prefix *_n.csv postfix numeration.") + + ->ClassElement(AZ::Edit::ClassElements::Group, "Data Settings") + + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerData::m_SaveFPSData, "Save FPS Data", + "When enabled, system will collect FPS data into csv.") + + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerData::m_SaveGPUData, "Save GPU Data", + "When enabled, system will collect GPU usage data into csv.") + + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerData::m_SaveCPUData, "Save CPU Data", + "When enabled, system will collect CPU usage data into csv.") + + ->ClassElement(AZ::Edit::ClassElements::Group, "Debug Settings") + + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerData::m_ShowFPS, "Show FPS", + "When enabled, system will show FPS counter in top-left corner."); + } + } + } +} // FPSProfiler \ No newline at end of file diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h new file mode 100644 index 00000000..f268a0f8 --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include +#include + +namespace FPSProfiler +{ + struct FPSProfilerData + { + AZ_TYPE_INFO(FPSProfilerData, FPSProfilerDataTypeId); + static void Reflect(AZ::ReflectContext* context); + + AZ::IO::Path m_OutputFilename = "@user@/fps_log.csv"; + bool m_SaveMultiple = true; + bool m_SaveFPSData = true; + bool m_SaveGPUData = true; + bool m_SaveCPUData = true; + bool m_ShowFPS = true; + }; +} // FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index b84b36cb..9339ac8c 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -1,31 +1,20 @@ #include "FPSProfilerEditorSystemComponent.h" -#include "AzQtComponents/Components/Widgets/FileDialog.h" -#include "UI/UICore/WidgetHelpers.h" - -#include - +#include #include namespace FPSProfiler { - AZ_COMPONENT_IMPL(FPSProfilerEditorSystemComponent, "FPSProfilerEditorSystemComponent", - FPSProfilerEditorSystemComponentTypeId, BaseSystemComponent); - void FPSProfilerEditorSystemComponent::Reflect(AZ::ReflectContext* context) { + FPSProfilerData::Reflect(context); + if (auto serializeContext = azrtti_cast(context)) { - serializeContext->Class() + serializeContext->Class() ->Version(0) - ->Field("m_OutputFilename", &FPSProfilerEditorSystemComponent::m_OutputFilename) - ->Field("m_SaveMultiple", &FPSProfilerEditorSystemComponent::m_SaveMultiple) - ->Field("m_SaveFPSData", &FPSProfilerEditorSystemComponent::m_SaveFPSData) - ->Field("m_SaveGPUData", &FPSProfilerEditorSystemComponent::m_SaveGPUData) - ->Field("m_SaveCPUData", &FPSProfilerEditorSystemComponent::m_SaveCPUData) - ->Field("m_ShowFPS", &FPSProfilerEditorSystemComponent::m_ShowFPS); - + ->Field("m_Configuration", &FPSProfilerEditorSystemComponent::m_Configuration); if (AZ::EditContext* editContext = serializeContext->GetEditContext()) { @@ -33,30 +22,8 @@ namespace FPSProfiler ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::Category, "Performance") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) - ->Attribute(AZ::Edit::Attributes::AutoExpand, false) - - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_OutputFilename, "Csv Save Path", "Select a path where *.csv will be saved.") - ->Attribute(AZ::Edit::Attributes::SourceAssetFilterPattern, "*.csv") - - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_SaveMultiple, "Save Multiple", - "When enabled, system will save files without overwriting current file. Each file will have prefix *_n.csv postfix numeration.") - - ->ClassElement(AZ::Edit::ClassElements::Group, "Data Settings") - - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_SaveFPSData, "Save FPS Data", - "When enabled, system will collect FPS data into csv.") - - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_SaveGPUData, "Save GPU Data", - "When enabled, system will collect GPU usage data into csv.") - - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_SaveCPUData, "Save CPU Data", - "When enabled, system will collect CPU usage data into csv.") - - ->ClassElement(AZ::Edit::ClassElements::Group, "Debug Settings") - - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_ShowFPS, "Show FPS", - "When enabled, system will show FPS counter in top-left corner.") - ; + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_Configuration); } } } @@ -67,35 +34,26 @@ namespace FPSProfiler void FPSProfilerEditorSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) { - BaseSystemComponent::GetProvidedServices(provided); provided.push_back(AZ_CRC_CE("FPSProfilerEditorService")); } void FPSProfilerEditorSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) { - BaseSystemComponent::GetIncompatibleServices(incompatible); incompatible.push_back(AZ_CRC_CE("FPSProfilerEditorService")); } - void FPSProfilerEditorSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) - { - BaseSystemComponent::GetRequiredServices(required); - } - - void FPSProfilerEditorSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent) - { - BaseSystemComponent::GetDependentServices(dependent); - } - void FPSProfilerEditorSystemComponent::Activate() { - FPSProfilerSystemComponent::Activate(); AzToolsFramework::EditorEvents::Bus::Handler::BusConnect(); } void FPSProfilerEditorSystemComponent::Deactivate() { AzToolsFramework::EditorEvents::Bus::Handler::BusDisconnect(); - FPSProfilerSystemComponent::Deactivate(); + } + + void FPSProfilerEditorSystemComponent::BuildGameEntity(AZ::Entity* entity) + { + entity->CreateComponent(m_Configuration); } } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h index 0ee0d9a0..e38c5c01 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h @@ -1,41 +1,36 @@ #pragma once -#include - +#include "FPSProfilerData.h" #include +#include +#include + namespace FPSProfiler { /// System component for FPSProfiler editor class FPSProfilerEditorSystemComponent - : public FPSProfilerSystemComponent + : public AzToolsFramework::Components::EditorComponentBase , protected AzToolsFramework::EditorEvents::Bus::Handler { - using BaseSystemComponent = FPSProfilerSystemComponent; public: - AZ_COMPONENT_DECL(FPSProfilerEditorSystemComponent); + AZ_EDITOR_COMPONENT(FPSProfilerEditorSystemComponent, FPSProfilerEditorSystemComponentTypeId, EditorComponentBase); static void Reflect(AZ::ReflectContext* context); FPSProfilerEditorSystemComponent(); ~FPSProfilerEditorSystemComponent(); - private: - static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); - static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); - static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); - static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); - // AZ::Component void Activate() override; void Deactivate() override; + void BuildGameEntity(AZ::Entity*) override; + + private: + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); - AZ::IO::Path m_OutputFilename = "@user@/fps_log.csv"; - bool m_SaveFPSData = true; - bool m_SaveMultiple = true; - bool m_SaveGPUData = true; - bool m_SaveCPUData = true; - bool m_ShowFPS = true; + FPSProfilerData m_Configuration; }; } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake index 934da04a..1c4c33b6 100644 --- a/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake +++ b/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake @@ -1,5 +1,7 @@ set(FILES + Source/Tools/FPSProfilerData.cpp + Source/Tools/FPSProfilerData.h Source/Tools/FPSProfilerEditorSystemComponent.cpp Source/Tools/FPSProfilerEditorSystemComponent.h ) From 95ae14074d0f3ffa498e598681774429d649188d Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 24 Feb 2025 10:54:17 +0100 Subject: [PATCH 003/175] rename fix Signed-off-by: Wojciech Czerski --- .../Source/Clients/FPSProfilerSystemComponent.cpp | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 77a71281..ca677f3f 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -19,8 +19,7 @@ namespace FPSProfiler if (auto serializeContext = azrtti_cast(context)) { serializeContext->Class() - ->Version(0) - ; + ->Version(0); } } @@ -63,7 +62,7 @@ namespace FPSProfiler } } - void FPSProfilerSystemComponent::Init() + void FPSProfilerSystemComponent::Init(q) { } @@ -74,7 +73,8 @@ namespace FPSProfiler if (m_ProfilerData.m_OutputFilename.empty()) { - m_ProfilerData.m_OutputFilename = "@user@/fps_log.csv"; // Default location in user log + AZ_Error("FPSProfiler", false, "The output filename must be provided or cannot be empty!"); + return; } AZ::IO::FileIOStream file(m_ProfilerData.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite); @@ -82,7 +82,7 @@ namespace FPSProfiler file.Write(csvHeader.size(), csvHeader.c_str()); file.Close(); - AZ_Printf("FPS Profiler", "FPS Profiler Activated on Level."); + AZ_Printf("FPS Profiler", "FPS Profiler Activated."); } void FPSProfilerSystemComponent::Deactivate() @@ -134,11 +134,6 @@ namespace FPSProfiler { if (!m_logEntries.empty()) { - for (int i = 0; i < sizeof(uint8_t); ++i) - { - - } - AZ::IO::FileIOStream file(m_ProfilerData.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeAppend | AZ::IO::OpenMode::ModeWrite); for (const auto& entry : m_logEntries) From aae4e6d4bb26ea6036491738f4d2c4dd26b7fb01 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 24 Feb 2025 10:55:04 +0100 Subject: [PATCH 004/175] remove unused Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 4 ---- .../Code/Source/Clients/FPSProfilerSystemComponent.h | 1 - 2 files changed, 5 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index ca677f3f..d81cfa48 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -62,10 +62,6 @@ namespace FPSProfiler } } - void FPSProfilerSystemComponent::Init(q) - { - } - void FPSProfilerSystemComponent::Activate() { FPSProfilerRequestBus::Handler::BusConnect(); diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index ca99798a..b3abe793 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -37,7 +37,6 @@ namespace FPSProfiler //////////////////////////////////////////////////////////////////////// // AZ::Component interface implementation - void Init() override; void Activate() override; void Deactivate() override; //////////////////////////////////////////////////////////////////////// From 504f7966e619434f3db3f4a5f1c10574d62ae846 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 24 Feb 2025 10:55:50 +0100 Subject: [PATCH 005/175] remove unused services Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 8 -------- .../Code/Source/Clients/FPSProfilerSystemComponent.h | 2 -- 2 files changed, 10 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index d81cfa48..cee13eab 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -33,14 +33,6 @@ namespace FPSProfiler incompatible.push_back(AZ_CRC_CE("FPSProfilerService")); } - void FPSProfilerSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) - { - } - - void FPSProfilerSystemComponent::GetDependentServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& dependent) - { - } - FPSProfilerSystemComponent::FPSProfilerSystemComponent() { if (FPSProfilerInterface::Get() == nullptr) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index b3abe793..cc3f022c 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -22,8 +22,6 @@ namespace FPSProfiler static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); - static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); - static void GetDependentServices(AZ::ComponentDescriptor::DependencyArrayType& dependent); FPSProfilerSystemComponent(); FPSProfilerSystemComponent(const FPSProfilerData& m_Configuration); From 3de9a4f6f28ab1b93007f8a33a29188ea2c66b84 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 24 Feb 2025 10:56:45 +0100 Subject: [PATCH 006/175] fix headers Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index cee13eab..a7561ace 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -1,12 +1,10 @@ #include "FPSProfilerSystemComponent.h" - -#include "Atom/RHI/RHISystemInterface.h" -#include "Atom/RPI.Public/RPISystemInterface.h" -#include "AzCore/IO/FileIO.h" - #include +#include +#include +#include #include namespace FPSProfiler From a795f7e30bbac4c50a0cf7a12e2dd153f106f9fa Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 24 Feb 2025 12:34:57 +0100 Subject: [PATCH 007/175] debug display on screen Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 31 ++++++++++++++++--- .../Clients/FPSProfilerSystemComponent.h | 10 ++++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index a7561ace..e4ce43e4 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace FPSProfiler { @@ -40,7 +41,7 @@ namespace FPSProfiler } FPSProfilerSystemComponent::FPSProfilerSystemComponent(const FPSProfilerData& m_Configuration) - : m_ProfilerData(m_Configuration) + : m_profilerData(m_Configuration) { } @@ -57,17 +58,23 @@ namespace FPSProfiler FPSProfilerRequestBus::Handler::BusConnect(); AZ::TickBus::Handler::BusConnect(); - if (m_ProfilerData.m_OutputFilename.empty()) + if (m_profilerData.m_OutputFilename.empty()) { AZ_Error("FPSProfiler", false, "The output filename must be provided or cannot be empty!"); return; } - AZ::IO::FileIOStream file(m_ProfilerData.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite); + AZ::IO::FileIOStream file(m_profilerData.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite); AZStd::string csvHeader = "Frame,FrameTime,InstantFPS,MinFPS,MaxFPS,AvgFPS,GpuMemoryUsed\n"; file.Write(csvHeader.size(), csvHeader.c_str()); file.Close(); + AzFramework::DebugDisplayRequestBus::BusPtr debugDisplayBus; + AzFramework::DebugDisplayRequestBus::Bind(debugDisplayBus, AzFramework::g_defaultSceneEntityDebugDisplayId); + m_debugDisplay = AzFramework::DebugDisplayRequestBus::FindFirstHandler(debugDisplayBus); + m_debugDisplay->SetColor(AZ::Colors::DarkRed); + m_debugDisplay->SetAlpha(0.8f); + AZ_Printf("FPS Profiler", "FPS Profiler Activated."); } @@ -82,6 +89,7 @@ namespace FPSProfiler void FPSProfilerSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { float fps = deltaTime > 0 ? (1.0f / deltaTime) : 0.0f; + ShowFPS(fps); m_fpsSamples.push_back(fps); m_minFPS = AZStd::min(m_minFPS, fps); @@ -120,7 +128,7 @@ namespace FPSProfiler { if (!m_logEntries.empty()) { - AZ::IO::FileIOStream file(m_ProfilerData.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeAppend | AZ::IO::OpenMode::ModeWrite); + AZ::IO::FileIOStream file(m_profilerData.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeAppend | AZ::IO::OpenMode::ModeWrite); for (const auto& entry : m_logEntries) { @@ -130,4 +138,19 @@ namespace FPSProfiler m_logEntries.clear(); } } + + void FPSProfilerSystemComponent::ShowFPS(const float& fps) const + { + if (!m_profilerData.m_ShowFPS) + { + return; + } + + AZStd::string debugText = AZStd::string::format("My Game | FPS: %.1f", fps); + + if (m_debugDisplay) + { + m_debugDisplay->Draw2dTextLabel(10, 10, 1.0f, debugText.c_str(), true); + } + } } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index cc3f022c..f3e1ee55 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -2,11 +2,12 @@ #pragma once #include +#include #include #include #include -#include +#include namespace FPSProfiler { @@ -53,9 +54,14 @@ namespace FPSProfiler float m_totalFrameTime = 0.0f; int m_frameCount = 0; - FPSProfilerData m_ProfilerData; + // Profiler Data - Editor Settings + FPSProfilerData m_profilerData; void WriteDataToFile(); + + // Debug display + AzFramework::DebugDisplayRequests* m_debugDisplay; + void ShowFPS(const float& fps) const; }; } // namespace FPSProfiler From 5da4f22c1628e419ba2ec425146ec4725f69bf4a Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 24 Feb 2025 13:01:05 +0100 Subject: [PATCH 008/175] add siplay fps on screen | add refleciton of config Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 18 +++++++++--------- .../Clients/FPSProfilerSystemComponent.h | 6 +++--- .../Tools/FPSProfilerEditorSystemComponent.cpp | 6 +++--- .../Tools/FPSProfilerEditorSystemComponent.h | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index e4ce43e4..2898bb00 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -18,7 +18,8 @@ namespace FPSProfiler if (auto serializeContext = azrtti_cast(context)) { serializeContext->Class() - ->Version(0); + ->Version(0) + ->Field("m_Configuration", &FPSProfilerSystemComponent::m_configuration); } } @@ -40,8 +41,8 @@ namespace FPSProfiler } } - FPSProfilerSystemComponent::FPSProfilerSystemComponent(const FPSProfilerData& m_Configuration) - : m_profilerData(m_Configuration) + FPSProfilerSystemComponent::FPSProfilerSystemComponent(const FPSProfilerData& m_configuration) + : m_configuration(m_configuration) { } @@ -58,13 +59,13 @@ namespace FPSProfiler FPSProfilerRequestBus::Handler::BusConnect(); AZ::TickBus::Handler::BusConnect(); - if (m_profilerData.m_OutputFilename.empty()) + if (m_configuration.m_OutputFilename.empty()) { AZ_Error("FPSProfiler", false, "The output filename must be provided or cannot be empty!"); return; } - AZ::IO::FileIOStream file(m_profilerData.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite); + AZ::IO::FileIOStream file(m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite); AZStd::string csvHeader = "Frame,FrameTime,InstantFPS,MinFPS,MaxFPS,AvgFPS,GpuMemoryUsed\n"; file.Write(csvHeader.size(), csvHeader.c_str()); file.Close(); @@ -128,7 +129,7 @@ namespace FPSProfiler { if (!m_logEntries.empty()) { - AZ::IO::FileIOStream file(m_profilerData.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeAppend | AZ::IO::OpenMode::ModeWrite); + AZ::IO::FileIOStream file(m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeAppend | AZ::IO::OpenMode::ModeWrite); for (const auto& entry : m_logEntries) { @@ -141,13 +142,12 @@ namespace FPSProfiler void FPSProfilerSystemComponent::ShowFPS(const float& fps) const { - if (!m_profilerData.m_ShowFPS) + if (!m_configuration.m_ShowFPS) { return; } - AZStd::string debugText = AZStd::string::format("My Game | FPS: %.1f", fps); - + AZStd::string debugText = AZStd::string::format("FPS: %.2f", fps); if (m_debugDisplay) { m_debugDisplay->Draw2dTextLabel(10, 10, 1.0f, debugText.c_str(), true); diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index f3e1ee55..2388df21 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -25,7 +25,7 @@ namespace FPSProfiler static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); FPSProfilerSystemComponent(); - FPSProfilerSystemComponent(const FPSProfilerData& m_Configuration); + explicit FPSProfilerSystemComponent(const FPSProfilerData& m_configuration); ~FPSProfilerSystemComponent(); protected: @@ -55,12 +55,12 @@ namespace FPSProfiler int m_frameCount = 0; // Profiler Data - Editor Settings - FPSProfilerData m_profilerData; + FPSProfilerData m_configuration; void WriteDataToFile(); // Debug display - AzFramework::DebugDisplayRequests* m_debugDisplay; + AzFramework::DebugDisplayRequests* m_debugDisplay = nullptr; void ShowFPS(const float& fps) const; }; diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 9339ac8c..011d68dc 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -14,7 +14,7 @@ namespace FPSProfiler { serializeContext->Class() ->Version(0) - ->Field("m_Configuration", &FPSProfilerEditorSystemComponent::m_Configuration); + ->Field("m_Configuration", &FPSProfilerEditorSystemComponent::m_configuration); if (AZ::EditContext* editContext = serializeContext->GetEditContext()) { @@ -23,7 +23,7 @@ namespace FPSProfiler ->Attribute(AZ::Edit::Attributes::Category, "Performance") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) ->Attribute(AZ::Edit::Attributes::AutoExpand, true) - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_Configuration); + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configuration); } } } @@ -54,6 +54,6 @@ namespace FPSProfiler void FPSProfilerEditorSystemComponent::BuildGameEntity(AZ::Entity* entity) { - entity->CreateComponent(m_Configuration); + entity->CreateComponent(m_configuration); } } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h index e38c5c01..ca9452f2 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h @@ -31,6 +31,6 @@ namespace FPSProfiler static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); - FPSProfilerData m_Configuration; + FPSProfilerData m_configuration; }; } // namespace FPSProfiler From 691cef64c9ca4b42fc3d4959011fcba544de0255 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 24 Feb 2025 13:28:04 +0100 Subject: [PATCH 009/175] improvements Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 17 +++++++++-------- .../Source/Clients/FPSProfilerSystemComponent.h | 10 ++++------ .../Code/Source/Tools/FPSProfilerData.cpp | 1 - .../Tools/FPSProfilerEditorSystemComponent.cpp | 5 ----- .../Tools/FPSProfilerEditorSystemComponent.h | 8 +++----- 5 files changed, 16 insertions(+), 25 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 2898bb00..c9091df4 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -1,6 +1,4 @@ - #include "FPSProfilerSystemComponent.h" -#include #include #include @@ -8,11 +6,10 @@ #include #include +#include + namespace FPSProfiler { - AZ_COMPONENT_IMPL(FPSProfilerSystemComponent, "FPSProfilerSystemComponent", - FPSProfilerSystemComponentTypeId); - void FPSProfilerSystemComponent::Reflect(AZ::ReflectContext* context) { if (auto serializeContext = azrtti_cast(context)) @@ -37,13 +34,17 @@ namespace FPSProfiler { if (FPSProfilerInterface::Get() == nullptr) { - FPSProfilerInterface::Register(this); + FPSProfilerInterface::Register(this); } } - FPSProfilerSystemComponent::FPSProfilerSystemComponent(const FPSProfilerData& m_configuration) - : m_configuration(m_configuration) + FPSProfilerSystemComponent::FPSProfilerSystemComponent(FPSProfilerData m_configuration) + : m_configuration(AZStd::move(m_configuration)) { + if (FPSProfilerInterface::Get() == nullptr) + { + FPSProfilerInterface::Register(this); + } } FPSProfilerSystemComponent::~FPSProfilerSystemComponent() diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 2388df21..146fe47e 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -1,4 +1,3 @@ - #pragma once #include @@ -11,13 +10,12 @@ namespace FPSProfiler { - class FPSProfilerSystemComponent - : public AZ::Component + class FPSProfilerSystemComponent : public AZ::Component , protected FPSProfilerRequestBus::Handler , public AZ::TickBus::Handler { public: - AZ_COMPONENT_DECL(FPSProfilerSystemComponent); + AZ_COMPONENT(FPSProfilerSystemComponent, FPSProfilerSystemComponentTypeId, Component); static void Reflect(AZ::ReflectContext* context); @@ -25,8 +23,8 @@ namespace FPSProfiler static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); FPSProfilerSystemComponent(); - explicit FPSProfilerSystemComponent(const FPSProfilerData& m_configuration); - ~FPSProfilerSystemComponent(); + explicit FPSProfilerSystemComponent(FPSProfilerData m_configuration); + ~FPSProfilerSystemComponent() override; protected: //////////////////////////////////////////////////////////////////////// diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp index 0c819fee..68eb7872 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp @@ -1,4 +1,3 @@ - #include "FPSProfilerData.h" #include diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 011d68dc..4a711908 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -1,4 +1,3 @@ - #include "FPSProfilerEditorSystemComponent.h" #include @@ -28,10 +27,6 @@ namespace FPSProfiler } } - FPSProfilerEditorSystemComponent::FPSProfilerEditorSystemComponent() = default; - - FPSProfilerEditorSystemComponent::~FPSProfilerEditorSystemComponent() = default; - void FPSProfilerEditorSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) { provided.push_back(AZ_CRC_CE("FPSProfilerEditorService")); diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h index ca9452f2..047da3d0 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h @@ -1,4 +1,3 @@ - #pragma once #include "FPSProfilerData.h" @@ -10,8 +9,7 @@ namespace FPSProfiler { /// System component for FPSProfiler editor - class FPSProfilerEditorSystemComponent - : public AzToolsFramework::Components::EditorComponentBase + class FPSProfilerEditorSystemComponent : public AzToolsFramework::Components::EditorComponentBase , protected AzToolsFramework::EditorEvents::Bus::Handler { public: @@ -19,8 +17,8 @@ namespace FPSProfiler static void Reflect(AZ::ReflectContext* context); - FPSProfilerEditorSystemComponent(); - ~FPSProfilerEditorSystemComponent(); + FPSProfilerEditorSystemComponent() = default; + ~FPSProfilerEditorSystemComponent() override = default; // AZ::Component void Activate() override; From 794f8e4fe72888c92cff57d8b31f660baf311cc0 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 24 Feb 2025 14:02:43 +0100 Subject: [PATCH 010/175] clean up Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index c9091df4..f5f6dfba 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -6,8 +6,6 @@ #include #include -#include - namespace FPSProfiler { void FPSProfilerSystemComponent::Reflect(AZ::ReflectContext* context) From 463d7b001e8bdcc7005c511d98043e36bf5bd6bb Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 24 Feb 2025 14:03:22 +0100 Subject: [PATCH 011/175] clang format Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 3 +- .../Code/Source/Clients/FPSProfilerModule.cpp | 7 ++- .../Clients/FPSProfilerSystemComponent.cpp | 15 +++--- .../Clients/FPSProfilerSystemComponent.h | 7 +-- .../Source/FPSProfilerModuleInterface.cpp | 9 ++-- .../Code/Source/FPSProfilerModuleInterface.h | 5 +- .../Code/Source/Tools/FPSProfilerData.cpp | 49 +++++++++++++------ .../Code/Source/Tools/FPSProfilerData.h | 4 +- .../Source/Tools/FPSProfilerEditorModule.cpp | 21 ++++---- .../FPSProfilerEditorSystemComponent.cpp | 8 +-- .../Tools/FPSProfilerEditorSystemComponent.h | 5 +- 11 files changed, 76 insertions(+), 57 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 26912fdd..4d1abc8d 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -16,8 +16,7 @@ namespace FPSProfiler // Put your public methods here }; - class FPSProfilerBusTraits - : public AZ::EBusTraits + class FPSProfilerBusTraits : public AZ::EBusTraits { public: ////////////////////////////////////////////////////////////////////////// diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerModule.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerModule.cpp index 0516b177..e55db965 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerModule.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerModule.cpp @@ -1,18 +1,17 @@ +#include "FPSProfilerSystemComponent.h" #include #include -#include "FPSProfilerSystemComponent.h" namespace FPSProfiler { - class FPSProfilerModule - : public FPSProfilerModuleInterface + class FPSProfilerModule : public FPSProfilerModuleInterface { public: AZ_RTTI(FPSProfilerModule, FPSProfilerModuleTypeId, FPSProfilerModuleInterface); AZ_CLASS_ALLOCATOR(FPSProfilerModule, AZ::SystemAllocator); }; -}// namespace FPSProfiler +} // namespace FPSProfiler #if defined(O3DE_GEM_NAME) AZ_DECLARE_MODULE_CLASS(AZ_JOIN(Gem_, O3DE_GEM_NAME), FPSProfiler::FPSProfilerModule) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index f5f6dfba..d3bf394d 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -12,9 +12,8 @@ namespace FPSProfiler { if (auto serializeContext = azrtti_cast(context)) { - serializeContext->Class() - ->Version(0) - ->Field("m_Configuration", &FPSProfilerSystemComponent::m_configuration); + serializeContext->Class()->Version(0)->Field( + "m_Configuration", &FPSProfilerSystemComponent::m_configuration); } } @@ -32,11 +31,11 @@ namespace FPSProfiler { if (FPSProfilerInterface::Get() == nullptr) { - FPSProfilerInterface::Register(this); + FPSProfilerInterface::Register(this); } } - FPSProfilerSystemComponent::FPSProfilerSystemComponent(FPSProfilerData m_configuration) + FPSProfilerSystemComponent::FPSProfilerSystemComponent(FPSProfilerData m_configuration) : m_configuration(AZStd::move(m_configuration)) { if (FPSProfilerInterface::Get() == nullptr) @@ -103,14 +102,12 @@ namespace FPSProfiler if (AZ::RPI::RPISystemInterface* rpiSystem = AZ::RPI::RPISystemInterface::Get()) { // gpuMemoryUsed = static_cast(rpiSystem->GetCurrentCpuMemoryUsage()); - [[maybe_unused]]AZ::RHI::RHISystemInterface* rhiSystem = AZ::RHI::RHISystemInterface::Get(); + [[maybe_unused]] AZ::RHI::RHISystemInterface* rhiSystem = AZ::RHI::RHISystemInterface::Get(); AZ::RHI::MemoryStatistics memoryStatistics; } AZStd::string logEntry = AZStd::string::format( - "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f\n", - m_frameCount, deltaTime, fps, m_minFPS, m_maxFPS, avgFPS, gpuMemoryUsed - ); + "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f\n", m_frameCount, deltaTime, fps, m_minFPS, m_maxFPS, avgFPS, gpuMemoryUsed); m_logEntries.push_back(logEntry); if (m_frameCount % 100 == 0) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 146fe47e..3cabd8f6 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -1,16 +1,17 @@ #pragma once -#include #include +#include -#include #include #include #include +#include namespace FPSProfiler { - class FPSProfilerSystemComponent : public AZ::Component + class FPSProfilerSystemComponent + : public AZ::Component , protected FPSProfilerRequestBus::Handler , public AZ::TickBus::Handler { diff --git a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp index b40677f2..d97f8e80 100644 --- a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp +++ b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp @@ -8,8 +8,7 @@ namespace FPSProfiler { - AZ_TYPE_INFO_WITH_NAME_IMPL(FPSProfilerModuleInterface, - "FPSProfilerModuleInterface", FPSProfilerModuleInterfaceTypeId); + AZ_TYPE_INFO_WITH_NAME_IMPL(FPSProfilerModuleInterface, "FPSProfilerModuleInterface", FPSProfilerModuleInterfaceTypeId); AZ_RTTI_NO_TYPE_INFO_IMPL(FPSProfilerModuleInterface, AZ::Module); AZ_CLASS_ALLOCATOR_IMPL(FPSProfilerModuleInterface, AZ::SystemAllocator); @@ -19,8 +18,10 @@ namespace FPSProfiler // Add ALL components descriptors associated with this gem to m_descriptors. // This will associate the AzTypeInfo information for the components with the the SerializeContext, BehaviorContext and EditContext. // This happens through the [MyComponent]::Reflect() function. - m_descriptors.insert(m_descriptors.end(), { - FPSProfilerSystemComponent::CreateDescriptor(), + m_descriptors.insert( + m_descriptors.end(), + { + FPSProfilerSystemComponent::CreateDescriptor(), }); } diff --git a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.h b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.h index c2737c5e..6595088e 100644 --- a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.h +++ b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.h @@ -6,8 +6,7 @@ namespace FPSProfiler { - class FPSProfilerModuleInterface - : public AZ::Module + class FPSProfilerModuleInterface : public AZ::Module { public: AZ_TYPE_INFO_WITH_NAME_DECL(FPSProfilerModuleInterface) @@ -21,4 +20,4 @@ namespace FPSProfiler */ AZ::ComponentTypeList GetRequiredSystemComponents() const override; }; -}// namespace FPSProfiler +} // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp index 68eb7872..565efd4a 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp @@ -19,34 +19,55 @@ namespace FPSProfiler if (AZ::EditContext* editContext = serializeContext->GetEditContext()) { - editContext->Class("FPS Profiler Configuration", "Tracks FPS, GPU and CPU performance and saves it into .csv") + editContext + ->Class("FPS Profiler Configuration", "Tracks FPS, GPU and CPU performance and saves it into .csv") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::Category, "Performance") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) ->Attribute(AZ::Edit::Attributes::AutoExpand, true) - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerData::m_OutputFilename, "Csv Save Path", "Select a path where *.csv will be saved.") - ->Attribute(AZ::Edit::Attributes::SourceAssetFilterPattern, "*.csv") + ->DataElement( + AZ::Edit::UIHandlers::Default, + &FPSProfilerData::m_OutputFilename, + "Csv Save Path", + "Select a path where *.csv will be saved.") + ->Attribute(AZ::Edit::Attributes::SourceAssetFilterPattern, "*.csv") - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerData::m_SaveMultiple, "Save Multiple", - "When enabled, system will save files without overwriting current file. Each file will have prefix *_n.csv postfix numeration.") + ->DataElement( + AZ::Edit::UIHandlers::Default, + &FPSProfilerData::m_SaveMultiple, + "Save Multiple", + "When enabled, system will save files without overwriting current file. Each file will have prefix *_n.csv postfix " + "numeration.") ->ClassElement(AZ::Edit::ClassElements::Group, "Data Settings") - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerData::m_SaveFPSData, "Save FPS Data", - "When enabled, system will collect FPS data into csv.") + ->DataElement( + AZ::Edit::UIHandlers::Default, + &FPSProfilerData::m_SaveFPSData, + "Save FPS Data", + "When enabled, system will collect FPS data into csv.") - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerData::m_SaveGPUData, "Save GPU Data", - "When enabled, system will collect GPU usage data into csv.") + ->DataElement( + AZ::Edit::UIHandlers::Default, + &FPSProfilerData::m_SaveGPUData, + "Save GPU Data", + "When enabled, system will collect GPU usage data into csv.") - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerData::m_SaveCPUData, "Save CPU Data", - "When enabled, system will collect CPU usage data into csv.") + ->DataElement( + AZ::Edit::UIHandlers::Default, + &FPSProfilerData::m_SaveCPUData, + "Save CPU Data", + "When enabled, system will collect CPU usage data into csv.") ->ClassElement(AZ::Edit::ClassElements::Group, "Debug Settings") - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerData::m_ShowFPS, "Show FPS", - "When enabled, system will show FPS counter in top-left corner."); + ->DataElement( + AZ::Edit::UIHandlers::Default, + &FPSProfilerData::m_ShowFPS, + "Show FPS", + "When enabled, system will show FPS counter in top-left corner."); } } } -} // FPSProfiler \ No newline at end of file +} // namespace FPSProfiler \ No newline at end of file diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h index f268a0f8..c30d130b 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h @@ -2,8 +2,8 @@ #include -#include #include +#include namespace FPSProfiler { @@ -19,4 +19,4 @@ namespace FPSProfiler bool m_SaveCPUData = true; bool m_ShowFPS = true; }; -} // FPSProfiler +} // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp index 7d8d546c..d83c271d 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp @@ -1,12 +1,11 @@ +#include "FPSProfilerEditorSystemComponent.h" #include #include -#include "FPSProfilerEditorSystemComponent.h" namespace FPSProfiler { - class FPSProfilerEditorModule - : public FPSProfilerModuleInterface + class FPSProfilerEditorModule : public FPSProfilerModuleInterface { public: AZ_RTTI(FPSProfilerEditorModule, FPSProfilerEditorModuleTypeId, FPSProfilerModuleInterface); @@ -16,11 +15,13 @@ namespace FPSProfiler { // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here. // Add ALL components descriptors associated with this gem to m_descriptors. - // This will associate the AzTypeInfo information for the components with the the SerializeContext, BehaviorContext and EditContext. - // This happens through the [MyComponent]::Reflect() function. - m_descriptors.insert(m_descriptors.end(), { - FPSProfilerEditorSystemComponent::CreateDescriptor(), - }); + // This will associate the AzTypeInfo information for the components with the the SerializeContext, BehaviorContext and + // EditContext. This happens through the [MyComponent]::Reflect() function. + m_descriptors.insert( + m_descriptors.end(), + { + FPSProfilerEditorSystemComponent::CreateDescriptor(), + }); } /** @@ -29,12 +30,12 @@ namespace FPSProfiler */ AZ::ComponentTypeList GetRequiredSystemComponents() const override { - return AZ::ComponentTypeList { + return AZ::ComponentTypeList{ azrtti_typeid(), }; } }; -}// namespace FPSProfiler +} // namespace FPSProfiler #if defined(O3DE_GEM_NAME) AZ_DECLARE_MODULE_CLASS(AZ_JOIN(Gem_, O3DE_GEM_NAME, _Editor), FPSProfiler::FPSProfilerEditorModule) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 4a711908..5d786af8 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -11,13 +11,13 @@ namespace FPSProfiler if (auto serializeContext = azrtti_cast(context)) { - serializeContext->Class() - ->Version(0) - ->Field("m_Configuration", &FPSProfilerEditorSystemComponent::m_configuration); + serializeContext->Class()->Version(0)->Field( + "m_Configuration", &FPSProfilerEditorSystemComponent::m_configuration); if (AZ::EditContext* editContext = serializeContext->GetEditContext()) { - editContext->Class("FPS Profiler", "Tracks FPS, GPU and CPU performance and saves it into .csv") + editContext + ->Class("FPS Profiler", "Tracks FPS, GPU and CPU performance and saves it into .csv") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::Category, "Performance") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h index 047da3d0..aac53fe5 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h @@ -3,13 +3,14 @@ #include "FPSProfilerData.h" #include -#include #include +#include namespace FPSProfiler { /// System component for FPSProfiler editor - class FPSProfilerEditorSystemComponent : public AzToolsFramework::Components::EditorComponentBase + class FPSProfilerEditorSystemComponent + : public AzToolsFramework::Components::EditorComponentBase , protected AzToolsFramework::EditorEvents::Bus::Handler { public: From 1a6b53700f0f0ac3798d5ab12145dfe1b5184a34 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 24 Feb 2025 14:13:03 +0100 Subject: [PATCH 012/175] fix game launcher ddebug display Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 23 +++++++++++-------- .../Clients/FPSProfilerSystemComponent.h | 1 - 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index d3bf394d..d9880c01 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -63,17 +63,11 @@ namespace FPSProfiler return; } - AZ::IO::FileIOStream file(m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite); + AZ::IO::FileIOStream file(m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath); AZStd::string csvHeader = "Frame,FrameTime,InstantFPS,MinFPS,MaxFPS,AvgFPS,GpuMemoryUsed\n"; file.Write(csvHeader.size(), csvHeader.c_str()); file.Close(); - AzFramework::DebugDisplayRequestBus::BusPtr debugDisplayBus; - AzFramework::DebugDisplayRequestBus::Bind(debugDisplayBus, AzFramework::g_defaultSceneEntityDebugDisplayId); - m_debugDisplay = AzFramework::DebugDisplayRequestBus::FindFirstHandler(debugDisplayBus); - m_debugDisplay->SetColor(AZ::Colors::DarkRed); - m_debugDisplay->SetAlpha(0.8f); - AZ_Printf("FPS Profiler", "FPS Profiler Activated."); } @@ -143,10 +137,19 @@ namespace FPSProfiler return; } - AZStd::string debugText = AZStd::string::format("FPS: %.2f", fps); - if (m_debugDisplay) + AzFramework::DebugDisplayRequestBus::BusPtr debugDisplayBus; + AzFramework::DebugDisplayRequestBus::Bind(debugDisplayBus, AzFramework::g_defaultSceneEntityDebugDisplayId); + AzFramework::DebugDisplayRequests* debugDisplay = AzFramework::DebugDisplayRequestBus::FindFirstHandler(debugDisplayBus); + + if (!debugDisplay) { - m_debugDisplay->Draw2dTextLabel(10, 10, 1.0f, debugText.c_str(), true); + return; } + + debugDisplay->SetColor(AZ::Colors::DarkRed); + debugDisplay->SetAlpha(0.8f); + + AZStd::string debugText = AZStd::string::format("FPS: %.2f", fps); + debugDisplay->Draw2dTextLabel(10, 10, 1.0f, debugText.c_str(), true); } } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 3cabd8f6..d82428e8 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -59,7 +59,6 @@ namespace FPSProfiler void WriteDataToFile(); // Debug display - AzFramework::DebugDisplayRequests* m_debugDisplay = nullptr; void ShowFPS(const float& fps) const; }; From d6731b2775058c7eee4b8eca77b545a6a7e9849b Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 25 Feb 2025 10:53:39 +0100 Subject: [PATCH 013/175] save with timestamp Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/CMakeLists.txt | 3 ++ .../Clients/FPSProfilerSystemComponent.cpp | 45 ++++++++++++++++--- .../Clients/FPSProfilerSystemComponent.h | 1 + .../Code/Source/Tools/FPSProfilerData.cpp | 4 +- .../Code/Source/Tools/FPSProfilerData.h | 2 +- 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/Gems/FPSProfiler/Code/CMakeLists.txt b/Gems/FPSProfiler/Code/CMakeLists.txt index 84258e64..313cbd55 100644 --- a/Gems/FPSProfiler/Code/CMakeLists.txt +++ b/Gems/FPSProfiler/Code/CMakeLists.txt @@ -30,6 +30,8 @@ ly_add_target( BUILD_DEPENDENCIES INTERFACE AZ::AzCore + AZ::AzFramework + AZ::AzToolsFramework Gem::Atom_RPI.Public Gem::Atom_RPI.Private Gem::Atom_RPI.Edit @@ -56,6 +58,7 @@ ly_add_target( PUBLIC AZ::AzCore AZ::AzFramework + AZ::AzToolsFramework Gem::Atom_RPI.Public Gem::Atom_RPI.Private Gem::Atom_RPI.Edit diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index d9880c01..a03120a3 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -1,5 +1,8 @@ #include "FPSProfilerSystemComponent.h" +#include +#include + #include #include #include @@ -63,10 +66,7 @@ namespace FPSProfiler return; } - AZ::IO::FileIOStream file(m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath); - AZStd::string csvHeader = "Frame,FrameTime,InstantFPS,MinFPS,MaxFPS,AvgFPS,GpuMemoryUsed\n"; - file.Write(csvHeader.size(), csvHeader.c_str()); - file.Close(); + CreateLogFile(); AZ_Printf("FPS Profiler", "FPS Profiler Activated."); } @@ -115,6 +115,41 @@ namespace FPSProfiler return AZ::TICK_GAME; } + void FPSProfilerSystemComponent::CreateLogFile() + { + AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); + bool fileExists = fileIO->Exists(m_configuration.m_OutputFilename.c_str()); + + if (!fileExists) + { + m_configuration.m_OutputFilename = "@user@/fps_log.csv"; // Restore to default path + } + + if (m_configuration.m_SaveWithTimestamp) + { + // Get current system time + auto now = AZStd::chrono::system_clock::now(); + std::time_t now_time_t = AZStd::chrono::system_clock::to_time_t(now); + + // Convert to local time structure + std::tm timeInfo; + localtime_r(&now_time_t, &timeInfo); + + // Format the timestamp as YYYYMMDD_HHMM + char timestamp[16]; // Buffer for formatted time + strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M", &timeInfo); + + m_configuration.m_OutputFilename.ReplaceFilename( + (m_configuration.m_OutputFilename.Stem().String() + "_" + timestamp + m_configuration.m_OutputFilename.Extension().String()) + .data()); + } + + AZ::IO::FileIOStream file(m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath); + AZStd::string csvHeader = "Frame,FrameTime,InstantFPS,MinFPS,MaxFPS,AvgFPS,GpuMemoryUsed\n"; + file.Write(csvHeader.size(), csvHeader.c_str()); + file.Close(); + } + void FPSProfilerSystemComponent::WriteDataToFile() { if (!m_logEntries.empty()) @@ -149,7 +184,7 @@ namespace FPSProfiler debugDisplay->SetColor(AZ::Colors::DarkRed); debugDisplay->SetAlpha(0.8f); - AZStd::string debugText = AZStd::string::format("FPS: %.2f", fps); + AZStd::string debugText = AZStd::string::format("Profiler | FPS: %.2f", fps); debugDisplay->Draw2dTextLabel(10, 10, 1.0f, debugText.c_str(), true); } } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index d82428e8..6b51bcb0 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -56,6 +56,7 @@ namespace FPSProfiler // Profiler Data - Editor Settings FPSProfilerData m_configuration; + void CreateLogFile(); void WriteDataToFile(); // Debug display diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp index 565efd4a..a43f7605 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp @@ -11,7 +11,7 @@ namespace FPSProfiler serializeContext->Class() ->Version(0) ->Field("m_OutputFilename", &FPSProfilerData::m_OutputFilename) - ->Field("m_SaveMultiple", &FPSProfilerData::m_SaveMultiple) + ->Field("m_SaveMultiple", &FPSProfilerData::m_SaveWithTimestamp) ->Field("m_SaveFPSData", &FPSProfilerData::m_SaveFPSData) ->Field("m_SaveCPUData", &FPSProfilerData::m_SaveCPUData) ->Field("m_SaveGPUData", &FPSProfilerData::m_SaveGPUData) @@ -35,7 +35,7 @@ namespace FPSProfiler ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerData::m_SaveMultiple, + &FPSProfilerData::m_SaveWithTimestamp, "Save Multiple", "When enabled, system will save files without overwriting current file. Each file will have prefix *_n.csv postfix " "numeration.") diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h index c30d130b..530f7fb3 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h @@ -13,7 +13,7 @@ namespace FPSProfiler static void Reflect(AZ::ReflectContext* context); AZ::IO::Path m_OutputFilename = "@user@/fps_log.csv"; - bool m_SaveMultiple = true; + bool m_SaveWithTimestamp = true; bool m_SaveFPSData = true; bool m_SaveGPUData = true; bool m_SaveCPUData = true; From 7e03adc0f5869adda54648b018d5533a76054e65 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 25 Feb 2025 10:54:33 +0100 Subject: [PATCH 014/175] change fps color Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index a03120a3..0f72fb67 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -181,8 +181,8 @@ namespace FPSProfiler return; } - debugDisplay->SetColor(AZ::Colors::DarkRed); - debugDisplay->SetAlpha(0.8f); + debugDisplay->SetColor(AZ::Colors::Red); + debugDisplay->SetAlpha(1.0f); AZStd::string debugText = AZStd::string::format("Profiler | FPS: %.2f", fps); debugDisplay->Draw2dTextLabel(10, 10, 1.0f, debugText.c_str(), true); From 67e57efc79c861f33a78ae25fbb048b1e8372bda Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 25 Feb 2025 11:09:19 +0100 Subject: [PATCH 015/175] Add notification bus for created files Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 31 +++++++++++++++++++ .../Include/FPSProfiler/FPSProfilerTypeIds.h | 1 + .../Clients/FPSProfilerSystemComponent.cpp | 9 ++++++ 3 files changed, 41 insertions(+) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 4d1abc8d..14ead149 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -5,6 +5,7 @@ #include #include +#include namespace FPSProfiler { @@ -29,4 +30,34 @@ namespace FPSProfiler using FPSProfilerRequestBus = AZ::EBus; using FPSProfilerInterface = AZ::Interface; + // Notifications EBus (For Sending Events) + class FPSProfilerNotifications + { + public: + AZ_RTTI(FPSProfilerNotifications, FPSProfilerNotificationsTypeId); + virtual ~FPSProfilerNotifications() = default; + + virtual void OnFileCreated(AZStd::string fileName) + { + } + virtual void OnFileUpdate(AZStd::string fileName) + { + } + virtual void OnFileSaved(AZStd::string fileName) + { + } + }; + + class FPSProfilerNotificationBusTraits : public AZ::EBusTraits + { + public: + ////////////////////////////////////////////////////////////////////////// + // EBusTraits overrides + static constexpr AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; // Allow multiple listeners + static constexpr AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; // Only one global address + ////////////////////////////////////////////////////////////////////////// + }; + + using FPSProfilerNotificationBus = AZ::EBus; + } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h index a56ce0ab..78d83c4c 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h @@ -18,4 +18,5 @@ namespace FPSProfiler // Interface TypeIds inline constexpr const char* FPSProfilerRequestsTypeId = "{D585EA71-B052-4C97-8647-4B3511CC7C5B}"; + inline constexpr const char* FPSProfilerNotificationsTypeId = "{63E04945-AD56-4BB6-888E-41C2FA71CC2F}"; } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 0f72fb67..fab62d4b 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -76,6 +76,9 @@ namespace FPSProfiler AZ::TickBus::Handler::BusDisconnect(); WriteDataToFile(); + // Notify - File Saved + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileSaved, m_configuration.m_OutputFilename.c_str()); + FPSProfilerRequestBus::Handler::BusDisconnect(); } @@ -148,6 +151,9 @@ namespace FPSProfiler AZStd::string csvHeader = "Frame,FrameTime,InstantFPS,MinFPS,MaxFPS,AvgFPS,GpuMemoryUsed\n"; file.Write(csvHeader.size(), csvHeader.c_str()); file.Close(); + + // Notify - File Created + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileCreated, m_configuration.m_OutputFilename.c_str()); } void FPSProfilerSystemComponent::WriteDataToFile() @@ -163,6 +169,9 @@ namespace FPSProfiler file.Close(); m_logEntries.clear(); } + + // Notify - File Update + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileUpdate, m_configuration.m_OutputFilename.c_str()); } void FPSProfilerSystemComponent::ShowFPS(const float& fps) const From f4109b20e5c0da16a8aa01e7bbe74d6b2ad3a38d Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 25 Feb 2025 11:11:45 +0100 Subject: [PATCH 016/175] add warning Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index fab62d4b..85a06769 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -126,6 +126,11 @@ namespace FPSProfiler if (!fileExists) { m_configuration.m_OutputFilename = "@user@/fps_log.csv"; // Restore to default path + AZ_Error( + "FPSProfiler::CreateLogFile", + false, + "Specified file does not exist. Using default path: %s", + m_configuration.m_OutputFilename); } if (m_configuration.m_SaveWithTimestamp) From 3c457a8d6ba220bb1d8898c306729f3521a81c4a Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 25 Feb 2025 11:12:23 +0100 Subject: [PATCH 017/175] fix warning - use c_str Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 85a06769..a7e2c077 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -130,7 +130,7 @@ namespace FPSProfiler "FPSProfiler::CreateLogFile", false, "Specified file does not exist. Using default path: %s", - m_configuration.m_OutputFilename); + m_configuration.m_OutputFilename.c_str()); } if (m_configuration.m_SaveWithTimestamp) From f45ffcccfd56b939dd98a6221ec802ff47737e19 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 25 Feb 2025 11:15:45 +0100 Subject: [PATCH 018/175] change name Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp index a43f7605..23ecc266 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp @@ -11,7 +11,7 @@ namespace FPSProfiler serializeContext->Class() ->Version(0) ->Field("m_OutputFilename", &FPSProfilerData::m_OutputFilename) - ->Field("m_SaveMultiple", &FPSProfilerData::m_SaveWithTimestamp) + ->Field("m_SaveWithTimestamp", &FPSProfilerData::m_SaveWithTimestamp) ->Field("m_SaveFPSData", &FPSProfilerData::m_SaveFPSData) ->Field("m_SaveCPUData", &FPSProfilerData::m_SaveCPUData) ->Field("m_SaveGPUData", &FPSProfilerData::m_SaveGPUData) @@ -36,9 +36,8 @@ namespace FPSProfiler ->DataElement( AZ::Edit::UIHandlers::Default, &FPSProfilerData::m_SaveWithTimestamp, - "Save Multiple", - "When enabled, system will save files without overwriting current file. Each file will have prefix *_n.csv postfix " - "numeration.") + "Save File With Timestamp", + "When enabled, system will save files with timestamp postfix of current date and hour.") ->ClassElement(AZ::Edit::ClassElements::Group, "Data Settings") From cb9579ff072a62e858f43d59778757e0c9c992a2 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 25 Feb 2025 11:18:41 +0100 Subject: [PATCH 019/175] add info in tick : Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index a7e2c077..d888422b 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -107,6 +107,7 @@ namespace FPSProfiler "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f\n", m_frameCount, deltaTime, fps, m_minFPS, m_maxFPS, avgFPS, gpuMemoryUsed); m_logEntries.push_back(logEntry); + // Save every 100 frames to not overflow buffer if (m_frameCount % 100 == 0) { WriteDataToFile(); From f0f2ead75e663aa4d8d8df385d6fe76baf23754a Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 25 Feb 2025 11:47:09 +0100 Subject: [PATCH 020/175] refactor Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index d888422b..81d5d365 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -164,17 +164,20 @@ namespace FPSProfiler void FPSProfilerSystemComponent::WriteDataToFile() { - if (!m_logEntries.empty()) + // Exit when nothing to save + if (m_logEntries.empty()) { - AZ::IO::FileIOStream file(m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeAppend | AZ::IO::OpenMode::ModeWrite); - - for (const auto& entry : m_logEntries) - { - file.Write(entry.size(), entry.c_str()); - } - file.Close(); - m_logEntries.clear(); + return; + } + + AZ::IO::FileIOStream file(m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeAppend); + + for (const auto& entry : m_logEntries) + { + file.Write(entry.size(), entry.c_str()); } + file.Close(); + m_logEntries.clear(); // Notify - File Update FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileUpdate, m_configuration.m_OutputFilename.c_str()); From f3aa038009eccdd91f7ba2f9ca4cb765712c5160 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 25 Feb 2025 12:09:38 +0100 Subject: [PATCH 021/175] fix calcuatlion of avergage frame Signed-off-by: Wojciech Czerski --- .../Source/Clients/FPSProfilerSystemComponent.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 81d5d365..def5fba8 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -1,8 +1,6 @@ #include "FPSProfilerSystemComponent.h" -#include -#include - +#include #include #include #include @@ -93,7 +91,10 @@ namespace FPSProfiler m_totalFrameTime += deltaTime; m_frameCount++; - float avgFPS = (m_frameCount > 0) ? (m_frameCount / m_totalFrameTime) : 0.0f; + // Average FPS calculation + float avgFPS = !m_fpsSamples.empty() ? + (AZStd::accumulate(m_fpsSamples.begin(), m_fpsSamples.end(), 0.0f) / m_fpsSamples.size()) + : 0.0f; float gpuMemoryUsed = 0.0f; if (AZ::RPI::RPISystemInterface* rpiSystem = AZ::RPI::RPISystemInterface::Get()) @@ -104,7 +105,7 @@ namespace FPSProfiler } AZStd::string logEntry = AZStd::string::format( - "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f\n", m_frameCount, deltaTime, fps, m_minFPS, m_maxFPS, avgFPS, gpuMemoryUsed); + "%d,%.4f,%.2f,%.2f,%.2f,%.4f,%.2f\n", m_frameCount, deltaTime, fps, m_minFPS, m_maxFPS, avgFPS, gpuMemoryUsed); m_logEntries.push_back(logEntry); // Save every 100 frames to not overflow buffer From 50ce4652c79fec362dcc4f7d1531c1e2a4cd7d05 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 25 Feb 2025 12:28:57 +0100 Subject: [PATCH 022/175] fix precision to near zero values Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 10 ++++++++-- .../Code/Source/Clients/FPSProfilerSystemComponent.h | 3 +-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index def5fba8..e031a583 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -86,11 +86,16 @@ namespace FPSProfiler ShowFPS(fps); m_fpsSamples.push_back(fps); - m_minFPS = AZStd::min(m_minFPS, fps); - m_maxFPS = AZStd::max(m_maxFPS, fps); m_totalFrameTime += deltaTime; m_frameCount++; + if (fps > 0.001f) // Ignore near zero values, precision to the third decimal after 0 + { + m_minFPS = AZStd::min(m_minFPS, fps); + } + + m_maxFPS = AZStd::max(m_maxFPS, fps); + // Average FPS calculation float avgFPS = !m_fpsSamples.empty() ? (AZStd::accumulate(m_fpsSamples.begin(), m_fpsSamples.end(), 0.0f) / m_fpsSamples.size()) @@ -179,6 +184,7 @@ namespace FPSProfiler } file.Close(); m_logEntries.clear(); + m_fpsSamples.clear(); // Notify - File Update FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileUpdate, m_configuration.m_OutputFilename.c_str()); diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 6b51bcb0..b3dbf9bb 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -6,7 +6,6 @@ #include #include #include -#include namespace FPSProfiler { @@ -48,7 +47,7 @@ namespace FPSProfiler AZStd::vector m_fpsSamples; AZStd::vector m_logEntries; - float m_minFPS = FLT_MAX; // Tracking the lowest FPS value - set to max for difference + float m_minFPS = AZ::Constants::FloatMax; // Tracking the lowest FPS value - set to max for difference float m_maxFPS = 0.0f; // Tracking the highest FPS value - set to min for diff float m_totalFrameTime = 0.0f; int m_frameCount = 0; From b34b3888bdb9518188c7a687aca0a5199d2a7100 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 25 Feb 2025 12:31:19 +0100 Subject: [PATCH 023/175] format Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index e031a583..f91556f0 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -1,10 +1,10 @@ #include "FPSProfilerSystemComponent.h" -#include #include #include #include #include +#include #include namespace FPSProfiler @@ -97,9 +97,8 @@ namespace FPSProfiler m_maxFPS = AZStd::max(m_maxFPS, fps); // Average FPS calculation - float avgFPS = !m_fpsSamples.empty() ? - (AZStd::accumulate(m_fpsSamples.begin(), m_fpsSamples.end(), 0.0f) / m_fpsSamples.size()) - : 0.0f; + float avgFPS = + !m_fpsSamples.empty() ? (AZStd::accumulate(m_fpsSamples.begin(), m_fpsSamples.end(), 0.0f) / m_fpsSamples.size()) : 0.0f; float gpuMemoryUsed = 0.0f; if (AZ::RPI::RPISystemInterface* rpiSystem = AZ::RPI::RPISystemInterface::Get()) @@ -110,7 +109,7 @@ namespace FPSProfiler } AZStd::string logEntry = AZStd::string::format( - "%d,%.4f,%.2f,%.2f,%.2f,%.4f,%.2f\n", m_frameCount, deltaTime, fps, m_minFPS, m_maxFPS, avgFPS, gpuMemoryUsed); + "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f\n", m_frameCount, deltaTime, fps, m_minFPS, m_maxFPS, avgFPS, gpuMemoryUsed); m_logEntries.push_back(logEntry); // Save every 100 frames to not overflow buffer From 7e6cd094e1d930b4e72639e40133abcb8b35f4a0 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 25 Feb 2025 12:47:39 +0100 Subject: [PATCH 024/175] add more editor options | specify precision and occurrance size Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 14 ++++++++--- .../Code/Source/Tools/FPSProfilerData.cpp | 23 +++++++++++++++++++ .../Code/Source/Tools/FPSProfilerData.h | 3 +++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index f91556f0..ed780d30 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -64,6 +64,14 @@ namespace FPSProfiler return; } + // Reserve at least twice as needed occurrences, since close and save operation may happen at the tick frame saves. + // Since log entries are cleared when occurrence update happens, it's good to reserve known size. + if (m_configuration.m_AutoSave) + { + m_fpsSamples.reserve(m_configuration.m_AutoSaveOccurrences * 2); + m_logEntries.reserve(m_configuration.m_AutoSaveOccurrences * 2); + } + CreateLogFile(); AZ_Printf("FPS Profiler", "FPS Profiler Activated."); @@ -89,7 +97,7 @@ namespace FPSProfiler m_totalFrameTime += deltaTime; m_frameCount++; - if (fps > 0.001f) // Ignore near zero values, precision to the third decimal after 0 + if (fps > m_configuration.m_NearZeroPrecision) // Ignore near zero values, precision to the third decimal after 0 { m_minFPS = AZStd::min(m_minFPS, fps); } @@ -112,8 +120,8 @@ namespace FPSProfiler "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f\n", m_frameCount, deltaTime, fps, m_minFPS, m_maxFPS, avgFPS, gpuMemoryUsed); m_logEntries.push_back(logEntry); - // Save every 100 frames to not overflow buffer - if (m_frameCount % 100 == 0) + // Save every 100 frames to not overflow buffer, when Auto Save enabled. + if (m_configuration.m_AutoSave && m_frameCount % 100 == 0) { WriteDataToFile(); } diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp index 23ecc266..ea39d5d9 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp @@ -12,6 +12,9 @@ namespace FPSProfiler ->Version(0) ->Field("m_OutputFilename", &FPSProfilerData::m_OutputFilename) ->Field("m_SaveWithTimestamp", &FPSProfilerData::m_SaveWithTimestamp) + ->Field("m_AutoSave", &FPSProfilerData::m_AutoSave) + ->Field("m_AutoSaveOccurrences", &FPSProfilerData::m_AutoSaveOccurrences) + ->Field("m_NearZeroPrecision", &FPSProfilerData::m_NearZeroPrecision) ->Field("m_SaveFPSData", &FPSProfilerData::m_SaveFPSData) ->Field("m_SaveCPUData", &FPSProfilerData::m_SaveCPUData) ->Field("m_SaveGPUData", &FPSProfilerData::m_SaveGPUData) @@ -39,6 +42,26 @@ namespace FPSProfiler "Save File With Timestamp", "When enabled, system will save files with timestamp postfix of current date and hour.") + ->DataElement( + AZ::Edit::UIHandlers::Default, + &FPSProfilerData::m_AutoSave, + "Auto Save", + "When enabled, system will auto save after specified frame occurencies.") + + ->DataElement( + AZ::Edit::UIHandlers::Default, + &FPSProfilerData::m_NearZeroPrecision, + "Near Zero Precision", + "Specify near Zero precision, that will be used for system.") + ->Attribute(AZ::Edit::Attributes::Min, 0.0f) + ->Attribute(AZ::Edit::Attributes::Max, 1.0f) + + ->DataElement( + AZ::Edit::UIHandlers::Default, + &FPSProfilerData::m_SaveWithTimestamp, + "Save File With Timestamp", + "When enabled, system will save files with timestamp postfix of current date and hour.") + ->ClassElement(AZ::Edit::ClassElements::Group, "Data Settings") ->DataElement( diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h index 530f7fb3..68c90a51 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h @@ -14,6 +14,9 @@ namespace FPSProfiler AZ::IO::Path m_OutputFilename = "@user@/fps_log.csv"; bool m_SaveWithTimestamp = true; + bool m_AutoSave = true; + float m_AutoSaveOccurrences = 100.0f; + float m_NearZeroPrecision = 0.01f; bool m_SaveFPSData = true; bool m_SaveGPUData = true; bool m_SaveCPUData = true; From b2ad8f129fa57ccf1a2e1b4c35717022c12da389 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 25 Feb 2025 12:50:02 +0100 Subject: [PATCH 025/175] clang format Signed-off-by: Wojciech Czerski --- .../Code/Source/Tools/FPSProfilerData.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp index ea39d5d9..ee7deae8 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp @@ -49,18 +49,18 @@ namespace FPSProfiler "When enabled, system will auto save after specified frame occurencies.") ->DataElement( - AZ::Edit::UIHandlers::Default, - &FPSProfilerData::m_NearZeroPrecision, - "Near Zero Precision", - "Specify near Zero precision, that will be used for system.") + AZ::Edit::UIHandlers::Default, + &FPSProfilerData::m_NearZeroPrecision, + "Near Zero Precision", + "Specify near Zero precision, that will be used for system.") ->Attribute(AZ::Edit::Attributes::Min, 0.0f) ->Attribute(AZ::Edit::Attributes::Max, 1.0f) ->DataElement( - AZ::Edit::UIHandlers::Default, - &FPSProfilerData::m_SaveWithTimestamp, - "Save File With Timestamp", - "When enabled, system will save files with timestamp postfix of current date and hour.") + AZ::Edit::UIHandlers::Default, + &FPSProfilerData::m_SaveWithTimestamp, + "Save File With Timestamp", + "When enabled, system will save files with timestamp postfix of current date and hour.") ->ClassElement(AZ::Edit::ClassElements::Group, "Data Settings") From a2d9d01b551c26afc279549f227bc3ddb7606b80 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 25 Feb 2025 13:40:56 +0100 Subject: [PATCH 026/175] add cpu and gpu memory collect Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 36 +++++++++++++++++-- .../Clients/FPSProfilerSystemComponent.h | 7 ++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index ed780d30..74f90159 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -1,5 +1,8 @@ #include "FPSProfilerSystemComponent.h" +#include "Atom/RPI.Public/RPIUtils.h" +#include "Atom/RPI.Public/ViewportContext.h" + #include #include #include @@ -117,7 +120,7 @@ namespace FPSProfiler } AZStd::string logEntry = AZStd::string::format( - "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f\n", m_frameCount, deltaTime, fps, m_minFPS, m_maxFPS, avgFPS, gpuMemoryUsed); + "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n", m_frameCount, deltaTime, fps, m_minFPS, m_maxFPS, avgFPS, gpuMemoryUsed, BytesToMB(GetCpuMemoryUsed()), BytesToMB(GetGpuMemoryUsed())); m_logEntries.push_back(logEntry); // Save every 100 frames to not overflow buffer, when Auto Save enabled. @@ -167,7 +170,7 @@ namespace FPSProfiler } AZ::IO::FileIOStream file(m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath); - AZStd::string csvHeader = "Frame,FrameTime,InstantFPS,MinFPS,MaxFPS,AvgFPS,GpuMemoryUsed\n"; + AZStd::string csvHeader = "Frame,FrameTime,CurrentFPS,MinFPS,MaxFPS,AvgFPS,CpuMemoryUsed,GpuMemoryUsed\n"; file.Write(csvHeader.size(), csvHeader.c_str()); file.Close(); @@ -197,6 +200,35 @@ namespace FPSProfiler FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileUpdate, m_configuration.m_OutputFilename.c_str()); } + size_t FPSProfilerSystemComponent::GetCpuMemoryUsed() + { + size_t usedBytes = 0; + size_t reservedBytes = 0; + + // Get stats for the system allocator + AZ::AllocatorManager::Instance().GetAllocatorStats(usedBytes, reservedBytes, nullptr); + + // Return the used bytes (allocated memory) + return usedBytes; + } + + size_t FPSProfilerSystemComponent::GetGpuMemoryUsed() + { + if (AZ::RHI::RHISystemInterface* rhiSystem = AZ::RHI::RHISystemInterface::Get()) + { + if (AZ::RHI::Device* device = rhiSystem->GetDevice()) + { + AZ::RHI::MemoryStatistics memoryStats; + device->CompileMemoryStatistics(memoryStats, AZ::RHI::MemoryStatisticsReportFlags::Detail); + + // Return the GPU memory used in bytes + return memoryStats.m_heaps.front().m_memoryUsage.m_totalResidentInBytes; + } + } + + return 0; + } + void FPSProfilerSystemComponent::ShowFPS(const float& fps) const { if (!m_configuration.m_ShowFPS) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index b3dbf9bb..b2ec6d5c 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -58,6 +58,13 @@ namespace FPSProfiler void CreateLogFile(); void WriteDataToFile(); + size_t GetCpuMemoryUsed(); + size_t GetGpuMemoryUsed(); + float BytesToMB(size_t bytes) + { + return static_cast(bytes) / (1024.0f * 1024.0f); + } + // Debug display void ShowFPS(const float& fps) const; }; From a9fa50f5f5d28699c54a4893ef5b7ff85532a8ab Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 25 Feb 2025 13:44:37 +0100 Subject: [PATCH 027/175] clean up tick | clang format Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 74f90159..ab532c13 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -111,16 +111,16 @@ namespace FPSProfiler float avgFPS = !m_fpsSamples.empty() ? (AZStd::accumulate(m_fpsSamples.begin(), m_fpsSamples.end(), 0.0f) / m_fpsSamples.size()) : 0.0f; - float gpuMemoryUsed = 0.0f; - if (AZ::RPI::RPISystemInterface* rpiSystem = AZ::RPI::RPISystemInterface::Get()) - { - // gpuMemoryUsed = static_cast(rpiSystem->GetCurrentCpuMemoryUsage()); - [[maybe_unused]] AZ::RHI::RHISystemInterface* rhiSystem = AZ::RHI::RHISystemInterface::Get(); - AZ::RHI::MemoryStatistics memoryStatistics; - } - AZStd::string logEntry = AZStd::string::format( - "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n", m_frameCount, deltaTime, fps, m_minFPS, m_maxFPS, avgFPS, gpuMemoryUsed, BytesToMB(GetCpuMemoryUsed()), BytesToMB(GetGpuMemoryUsed())); + "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n", + m_frameCount, + deltaTime, + fps, + m_minFPS, + m_maxFPS, + avgFPS, + BytesToMB(GetCpuMemoryUsed()), + BytesToMB(GetGpuMemoryUsed())); m_logEntries.push_back(logEntry); // Save every 100 frames to not overflow buffer, when Auto Save enabled. From db92a4902045cc38b0013a2b90aff57680ab6f20 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 25 Feb 2025 13:50:06 +0100 Subject: [PATCH 028/175] make memory getter functions static Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 +- .../Code/Source/Clients/FPSProfilerSystemComponent.h | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index ab532c13..462cad8b 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -219,7 +219,7 @@ namespace FPSProfiler if (AZ::RHI::Device* device = rhiSystem->GetDevice()) { AZ::RHI::MemoryStatistics memoryStats; - device->CompileMemoryStatistics(memoryStats, AZ::RHI::MemoryStatisticsReportFlags::Detail); + device->CompileMemoryStatistics(memoryStats, AZ::RHI::MemoryStatisticsReportFlags::Basic); // Return the GPU memory used in bytes return memoryStats.m_heaps.front().m_memoryUsage.m_totalResidentInBytes; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index b2ec6d5c..4c179325 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -58,9 +58,10 @@ namespace FPSProfiler void CreateLogFile(); void WriteDataToFile(); - size_t GetCpuMemoryUsed(); - size_t GetGpuMemoryUsed(); - float BytesToMB(size_t bytes) + static size_t GetCpuMemoryUsed(); + static size_t GetGpuMemoryUsed(); + + static float BytesToMB(size_t bytes) { return static_cast(bytes) / (1024.0f * 1024.0f); } From 51a18fb837097b396318b36156c4412b17641770 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 25 Feb 2025 13:50:27 +0100 Subject: [PATCH 029/175] use detailed memory flag Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 462cad8b..ab532c13 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -219,7 +219,7 @@ namespace FPSProfiler if (AZ::RHI::Device* device = rhiSystem->GetDevice()) { AZ::RHI::MemoryStatistics memoryStats; - device->CompileMemoryStatistics(memoryStats, AZ::RHI::MemoryStatisticsReportFlags::Basic); + device->CompileMemoryStatistics(memoryStats, AZ::RHI::MemoryStatisticsReportFlags::Detail); // Return the GPU memory used in bytes return memoryStats.m_heaps.front().m_memoryUsage.m_totalResidentInBytes; From 6968a678237bf9e2752bee457eaf6fbd2d02ba28 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 27 Feb 2025 12:02:32 +0100 Subject: [PATCH 030/175] fix fps data struct reflection | fix system component, clean up, adjust to config Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 63 +++++++++++-------- .../Clients/FPSProfilerSystemComponent.h | 18 +++--- .../Code/Source/Tools/FPSProfilerData.cpp | 32 +++++++--- .../Code/Source/Tools/FPSProfilerData.h | 2 +- 4 files changed, 71 insertions(+), 44 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index ab532c13..5c7c74b6 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -93,38 +93,31 @@ namespace FPSProfiler void FPSProfilerSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { - float fps = deltaTime > 0 ? (1.0f / deltaTime) : 0.0f; - ShowFPS(fps); - - m_fpsSamples.push_back(fps); - m_totalFrameTime += deltaTime; - m_frameCount++; - - if (fps > m_configuration.m_NearZeroPrecision) // Ignore near zero values, precision to the third decimal after 0 + if (m_configuration.m_ShowFPS) { - m_minFPS = AZStd::min(m_minFPS, fps); + ShowFps(); } - m_maxFPS = AZStd::max(m_maxFPS, fps); - - // Average FPS calculation - float avgFPS = - !m_fpsSamples.empty() ? (AZStd::accumulate(m_fpsSamples.begin(), m_fpsSamples.end(), 0.0f) / m_fpsSamples.size()) : 0.0f; + // Calculate data only if enabled, otherwise push default values to log entry. + if (m_configuration.m_SaveFPSData) + { + CalculateFpsData(deltaTime); + } AZStd::string logEntry = AZStd::string::format( "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n", m_frameCount, deltaTime, - fps, + m_currentFPS, m_minFPS, m_maxFPS, - avgFPS, - BytesToMB(GetCpuMemoryUsed()), - BytesToMB(GetGpuMemoryUsed())); + m_avgFPS, + m_configuration.m_SaveCPUData ? BytesToMB(GetCpuMemoryUsed()) : 0.0f, + m_configuration.m_SaveGPUData ? BytesToMB(GetGpuMemoryUsed()) : 0.0f); m_logEntries.push_back(logEntry); - // Save every 100 frames to not overflow buffer, when Auto Save enabled. - if (m_configuration.m_AutoSave && m_frameCount % 100 == 0) + // Save after every m_AutoSaveOccurrences frames to not overflow buffer, only when m_AutoSave enabled. + if (m_configuration.m_AutoSave && m_frameCount % m_configuration.m_AutoSaveOccurrences == 0) { WriteDataToFile(); } @@ -135,6 +128,24 @@ namespace FPSProfiler return AZ::TICK_GAME; } + void FPSProfilerSystemComponent::CalculateFpsData(const float& deltaTime) + { + m_currentFPS = deltaTime > 0 ? (1.0f / deltaTime) : 0.0f; + m_fpsSamples.push_back(m_currentFPS); + + m_totalFrameTime += deltaTime; + m_frameCount++; + + // Ignore near zero values, precision to the third decimal after 0 + if (m_currentFPS > m_configuration.m_NearZeroPrecision) + { + m_minFPS = AZStd::min(m_minFPS, m_currentFPS); + } + m_maxFPS = AZStd::max(m_maxFPS, m_currentFPS); + + m_avgFPS = !m_fpsSamples.empty() ? (AZStd::accumulate(m_fpsSamples.begin(), m_fpsSamples.end(), 0.0f) / m_fpsSamples.size()) : 0.0f; + } + void FPSProfilerSystemComponent::CreateLogFile() { AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); @@ -229,13 +240,13 @@ namespace FPSProfiler return 0; } - void FPSProfilerSystemComponent::ShowFPS(const float& fps) const + float FPSProfilerSystemComponent::BytesToMB(size_t bytes) { - if (!m_configuration.m_ShowFPS) - { - return; - } + return static_cast(bytes) / (1024.0f * 1024.0f); + } + void FPSProfilerSystemComponent::ShowFps() const + { AzFramework::DebugDisplayRequestBus::BusPtr debugDisplayBus; AzFramework::DebugDisplayRequestBus::Bind(debugDisplayBus, AzFramework::g_defaultSceneEntityDebugDisplayId); AzFramework::DebugDisplayRequests* debugDisplay = AzFramework::DebugDisplayRequestBus::FindFirstHandler(debugDisplayBus); @@ -248,7 +259,7 @@ namespace FPSProfiler debugDisplay->SetColor(AZ::Colors::Red); debugDisplay->SetAlpha(1.0f); - AZStd::string debugText = AZStd::string::format("Profiler | FPS: %.2f", fps); + AZStd::string debugText = AZStd::string::format("Profiler | FPS: %.2f", m_currentFPS); debugDisplay->Draw2dTextLabel(10, 10, 1.0f, debugText.c_str(), true); } } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 4c179325..eae86241 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -44,30 +44,32 @@ namespace FPSProfiler int GetTickOrder() override; //////////////////////////////////////////////////////////////////////// private: - AZStd::vector m_fpsSamples; - AZStd::vector m_logEntries; + AZStd::vector m_fpsSamples; // Vector of collected current FPSs. Cleared once @ref m_configuration.m_AutoSave enabled. + AZStd::vector m_logEntries; // Vector of collected log entries. Cleared after @ref + // m_configuration.m_AutoSaveOccurrences, when @ref m_configuration.m_AutoSave enabled. float m_minFPS = AZ::Constants::FloatMax; // Tracking the lowest FPS value - set to max for difference float m_maxFPS = 0.0f; // Tracking the highest FPS value - set to min for diff + float m_avgFPS = 0.0f; // Mean Value of accumulated current FPS + float m_currentFPS = 0.0f; // Actual FPS in current frame float m_totalFrameTime = 0.0f; int m_frameCount = 0; + void CalculateFpsData(const float& deltaTime); // Profiler Data - Editor Settings FPSProfilerData m_configuration; + // File operations void CreateLogFile(); void WriteDataToFile(); + // Memory Access static size_t GetCpuMemoryUsed(); static size_t GetGpuMemoryUsed(); - - static float BytesToMB(size_t bytes) - { - return static_cast(bytes) / (1024.0f * 1024.0f); - } + static float BytesToMB(size_t bytes); // Debug display - void ShowFPS(const float& fps) const; + void ShowFps() const; }; } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp index ee7deae8..0ed262dd 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp @@ -6,6 +6,11 @@ namespace FPSProfiler { void FPSProfilerData::Reflect(AZ::ReflectContext* context) { + if (!context) + { + return; + } + if (auto serializeContext = azrtti_cast(context)) { serializeContext->Class() @@ -39,14 +44,29 @@ namespace FPSProfiler ->DataElement( AZ::Edit::UIHandlers::Default, &FPSProfilerData::m_SaveWithTimestamp, - "Save File With Timestamp", + "Timestamp", "When enabled, system will save files with timestamp postfix of current date and hour.") ->DataElement( AZ::Edit::UIHandlers::Default, &FPSProfilerData::m_AutoSave, "Auto Save", - "When enabled, system will auto save after specified frame occurencies.") + "When enabled, system will auto save after specified frame occurrance.") + ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree) + + ->DataElement( + AZ::Edit::UIHandlers::Default, + &FPSProfilerData::m_AutoSaveOccurrences, + "Auto Save At Frame", + "Specify after how many frames system will auto save log.") + ->Attribute(AZ::Edit::Attributes::Min, 1) + ->Attribute( + AZ::Edit::Attributes::Visibility, + [](const void* instance) + { + const FPSProfilerData* data = reinterpret_cast(instance); + return data && data->m_AutoSave ? AZ::Edit::PropertyVisibility::Show : AZ::Edit::PropertyVisibility::Hide; + }) ->DataElement( AZ::Edit::UIHandlers::Default, @@ -54,13 +74,7 @@ namespace FPSProfiler "Near Zero Precision", "Specify near Zero precision, that will be used for system.") ->Attribute(AZ::Edit::Attributes::Min, 0.0f) - ->Attribute(AZ::Edit::Attributes::Max, 1.0f) - - ->DataElement( - AZ::Edit::UIHandlers::Default, - &FPSProfilerData::m_SaveWithTimestamp, - "Save File With Timestamp", - "When enabled, system will save files with timestamp postfix of current date and hour.") + ->Attribute(AZ::Edit::Attributes::Max, 0.1f) ->ClassElement(AZ::Edit::ClassElements::Group, "Data Settings") diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h index 68c90a51..1c9f7247 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h @@ -15,7 +15,7 @@ namespace FPSProfiler AZ::IO::Path m_OutputFilename = "@user@/fps_log.csv"; bool m_SaveWithTimestamp = true; bool m_AutoSave = true; - float m_AutoSaveOccurrences = 100.0f; + int m_AutoSaveOccurrences = 100; float m_NearZeroPrecision = 0.01f; bool m_SaveFPSData = true; bool m_SaveGPUData = true; From 76c2b6ae4dcf5636a7d1529dc91de4a81d67598e Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 27 Feb 2025 12:13:20 +0100 Subject: [PATCH 031/175] refactor Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 31 ++++++++-------- .../Clients/FPSProfilerSystemComponent.h | 36 ++++++++----------- 2 files changed, 28 insertions(+), 39 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 5c7c74b6..eb673155 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -58,9 +58,6 @@ namespace FPSProfiler void FPSProfilerSystemComponent::Activate() { - FPSProfilerRequestBus::Handler::BusConnect(); - AZ::TickBus::Handler::BusConnect(); - if (m_configuration.m_OutputFilename.empty()) { AZ_Error("FPSProfiler", false, "The output filename must be provided or cannot be empty!"); @@ -75,9 +72,9 @@ namespace FPSProfiler m_logEntries.reserve(m_configuration.m_AutoSaveOccurrences * 2); } + FPSProfilerRequestBus::Handler::BusConnect(); + AZ::TickBus::Handler::BusConnect(); CreateLogFile(); - - AZ_Printf("FPS Profiler", "FPS Profiler Activated."); } void FPSProfilerSystemComponent::Deactivate() @@ -108,10 +105,10 @@ namespace FPSProfiler "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n", m_frameCount, deltaTime, - m_currentFPS, - m_minFPS, - m_maxFPS, - m_avgFPS, + m_currentFps, + m_minFps, + m_maxFps, + m_avgFps, m_configuration.m_SaveCPUData ? BytesToMB(GetCpuMemoryUsed()) : 0.0f, m_configuration.m_SaveGPUData ? BytesToMB(GetGpuMemoryUsed()) : 0.0f); m_logEntries.push_back(logEntry); @@ -130,20 +127,20 @@ namespace FPSProfiler void FPSProfilerSystemComponent::CalculateFpsData(const float& deltaTime) { - m_currentFPS = deltaTime > 0 ? (1.0f / deltaTime) : 0.0f; - m_fpsSamples.push_back(m_currentFPS); + m_currentFps = deltaTime > 0 ? (1.0f / deltaTime) : 0.0f; + m_fpsSamples.push_back(m_currentFps); m_totalFrameTime += deltaTime; m_frameCount++; - // Ignore near zero values, precision to the third decimal after 0 - if (m_currentFPS > m_configuration.m_NearZeroPrecision) + // Using m_NearZeroPrecision, since m_currentFPS cannot be equal to 0 if delta time is valid. + if (m_currentFps > m_configuration.m_NearZeroPrecision) { - m_minFPS = AZStd::min(m_minFPS, m_currentFPS); + m_minFps = AZStd::min(m_minFps, m_currentFps); } - m_maxFPS = AZStd::max(m_maxFPS, m_currentFPS); + m_maxFps = AZStd::max(m_maxFps, m_currentFps); - m_avgFPS = !m_fpsSamples.empty() ? (AZStd::accumulate(m_fpsSamples.begin(), m_fpsSamples.end(), 0.0f) / m_fpsSamples.size()) : 0.0f; + m_avgFps = !m_fpsSamples.empty() ? (AZStd::accumulate(m_fpsSamples.begin(), m_fpsSamples.end(), 0.0f) / m_fpsSamples.size()) : 0.0f; } void FPSProfilerSystemComponent::CreateLogFile() @@ -259,7 +256,7 @@ namespace FPSProfiler debugDisplay->SetColor(AZ::Colors::Red); debugDisplay->SetAlpha(1.0f); - AZStd::string debugText = AZStd::string::format("Profiler | FPS: %.2f", m_currentFPS); + AZStd::string debugText = AZStd::string::format("Profiler | FPS: %.2f", m_currentFps); debugDisplay->Draw2dTextLabel(10, 10, 1.0f, debugText.c_str(), true); } } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index eae86241..a13e0b0f 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -27,49 +27,41 @@ namespace FPSProfiler ~FPSProfilerSystemComponent() override; protected: - //////////////////////////////////////////////////////////////////////// - // FPSProfilerRequestBus interface implementation - - //////////////////////////////////////////////////////////////////////// - - //////////////////////////////////////////////////////////////////////// // AZ::Component interface implementation void Activate() override; void Deactivate() override; - //////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////// // AZTickBus interface implementation void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; int GetTickOrder() override; - //////////////////////////////////////////////////////////////////////// + private: - AZStd::vector m_fpsSamples; // Vector of collected current FPSs. Cleared once @ref m_configuration.m_AutoSave enabled. - AZStd::vector m_logEntries; // Vector of collected log entries. Cleared after @ref - // m_configuration.m_AutoSaveOccurrences, when @ref m_configuration.m_AutoSave enabled. + // Profiler Data - Editor Settings + FPSProfilerData m_configuration; - float m_minFPS = AZ::Constants::FloatMax; // Tracking the lowest FPS value - set to max for difference - float m_maxFPS = 0.0f; // Tracking the highest FPS value - set to min for diff - float m_avgFPS = 0.0f; // Mean Value of accumulated current FPS - float m_currentFPS = 0.0f; // Actual FPS in current frame + float m_minFps = AZ::Constants::FloatMax; // Tracking the lowest FPS value - set to max for difference + float m_maxFps = 0.0f; // Tracking the highest FPS value - set to min for diff + float m_avgFps = 0.0f; // Mean Value of accumulated current FPS + float m_currentFps = 0.0f; // Actual FPS in current frame float m_totalFrameTime = 0.0f; int m_frameCount = 0; - void CalculateFpsData(const float& deltaTime); - - // Profiler Data - Editor Settings - FPSProfilerData m_configuration; + AZStd::vector m_fpsSamples; // Vector of collected current FPSs. Cleared once @ref m_configuration.m_AutoSave enabled. + AZStd::vector m_logEntries; // Vector of collected log entries. Cleared after @ref + // m_configuration.m_AutoSaveOccurrences, when @ref m_configuration.m_AutoSave enabled. // File operations void CreateLogFile(); void WriteDataToFile(); + // Helpers + void CalculateFpsData(const float& deltaTime); + static float BytesToMB(size_t bytes); + // Memory Access static size_t GetCpuMemoryUsed(); static size_t GetGpuMemoryUsed(); - static float BytesToMB(size_t bytes); // Debug display void ShowFps() const; }; - } // namespace FPSProfiler From 7e3fdb2e358b2f6d0bc59ca3eff488e42b7f459c Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 27 Feb 2025 12:22:40 +0100 Subject: [PATCH 032/175] refactor Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 6 ++++++ .../Code/Source/Clients/FPSProfilerSystemComponent.h | 10 +++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index eb673155..cc397881 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -72,6 +72,12 @@ namespace FPSProfiler m_logEntries.reserve(m_configuration.m_AutoSaveOccurrences * 2); } + if (m_configuration.m_SaveFPSData) + { + // Cannot be 0 for std::min comparison + m_minFps = AZ::Constants::FloatMax; + } + FPSProfilerRequestBus::Handler::BusConnect(); AZ::TickBus::Handler::BusConnect(); CreateLogFile(); diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index a13e0b0f..3bf8770f 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -9,7 +9,7 @@ namespace FPSProfiler { - class FPSProfilerSystemComponent + class FPSProfilerSystemComponent final : public AZ::Component , protected FPSProfilerRequestBus::Handler , public AZ::TickBus::Handler @@ -39,12 +39,12 @@ namespace FPSProfiler // Profiler Data - Editor Settings FPSProfilerData m_configuration; - float m_minFps = AZ::Constants::FloatMax; // Tracking the lowest FPS value - set to max for difference - float m_maxFps = 0.0f; // Tracking the highest FPS value - set to min for diff + float m_minFps = 0.0; // Tracking the lowest FPS value + float m_maxFps = 0.0f; // Tracking the highest FPS value float m_avgFps = 0.0f; // Mean Value of accumulated current FPS float m_currentFps = 0.0f; // Actual FPS in current frame - float m_totalFrameTime = 0.0f; - int m_frameCount = 0; + float m_totalFrameTime = 0.0f; // Time it took to enter frame + int m_frameCount = 0; // Numeric value of actual frame AZStd::vector m_fpsSamples; // Vector of collected current FPSs. Cleared once @ref m_configuration.m_AutoSave enabled. AZStd::vector m_logEntries; // Vector of collected log entries. Cleared after @ref // m_configuration.m_AutoSaveOccurrences, when @ref m_configuration.m_AutoSave enabled. From 5aa41f2108b252f13cdb18103444534fb1eafcbe Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 27 Feb 2025 13:21:31 +0100 Subject: [PATCH 033/175] refactor | remove redundant headers | remove rpi from cmake, use only rhi public Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/CMakeLists.txt | 12 ------------ .../Code/Include/FPSProfiler/FPSProfilerBus.h | 1 - .../Source/Clients/FPSProfilerSystemComponent.cpp | 7 ++----- .../Code/Source/Tools/FPSProfilerData.cpp | 5 ----- 4 files changed, 2 insertions(+), 23 deletions(-) diff --git a/Gems/FPSProfiler/Code/CMakeLists.txt b/Gems/FPSProfiler/Code/CMakeLists.txt index 313cbd55..4e341d09 100644 --- a/Gems/FPSProfiler/Code/CMakeLists.txt +++ b/Gems/FPSProfiler/Code/CMakeLists.txt @@ -31,13 +31,6 @@ ly_add_target( INTERFACE AZ::AzCore AZ::AzFramework - AZ::AzToolsFramework - Gem::Atom_RPI.Public - Gem::Atom_RPI.Private - Gem::Atom_RPI.Edit - Gem::Atom_RHI.Public - Gem::Atom_RHI.Private - Gem::Atom_RHI.Edit ) # The ${gem_name}.Private.Object target is an internal target @@ -59,12 +52,7 @@ ly_add_target( AZ::AzCore AZ::AzFramework AZ::AzToolsFramework - Gem::Atom_RPI.Public - Gem::Atom_RPI.Private - Gem::Atom_RPI.Edit Gem::Atom_RHI.Public - Gem::Atom_RHI.Private - Gem::Atom_RHI.Edit ) # Here add ${gem_name} target, it depends on the Private Object library and Public API interface diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 14ead149..f0f004e5 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -1,4 +1,3 @@ - #pragma once #include diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index cc397881..e83673b3 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -1,14 +1,11 @@ #include "FPSProfilerSystemComponent.h" -#include "Atom/RPI.Public/RPIUtils.h" -#include "Atom/RPI.Public/ViewportContext.h" - +#include +#include #include -#include #include #include #include -#include namespace FPSProfiler { diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp index 0ed262dd..08616936 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp @@ -6,11 +6,6 @@ namespace FPSProfiler { void FPSProfilerData::Reflect(AZ::ReflectContext* context) { - if (!context) - { - return; - } - if (auto serializeContext = azrtti_cast(context)) { serializeContext->Class() From 38f2e6a3ef3ee80783fa9bc80eb3c8a27f42302d Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 27 Feb 2025 13:26:08 +0100 Subject: [PATCH 034/175] rename to Fps | use const reference for notification bus Signed-off-by: Wojciech Czerski --- .../FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h | 6 +++--- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 6 +++--- Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp | 8 ++++---- Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index f0f004e5..00fdf29d 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -36,13 +36,13 @@ namespace FPSProfiler AZ_RTTI(FPSProfilerNotifications, FPSProfilerNotificationsTypeId); virtual ~FPSProfilerNotifications() = default; - virtual void OnFileCreated(AZStd::string fileName) + virtual void OnFileCreated(const AZStd::string& fileName) { } - virtual void OnFileUpdate(AZStd::string fileName) + virtual void OnFileUpdate(const AZStd::string& fileName) { } - virtual void OnFileSaved(AZStd::string fileName) + virtual void OnFileSaved(const AZStd::string& fileName) { } }; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index e83673b3..b938e692 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -69,7 +69,7 @@ namespace FPSProfiler m_logEntries.reserve(m_configuration.m_AutoSaveOccurrences * 2); } - if (m_configuration.m_SaveFPSData) + if (m_configuration.m_SaveFpsData) { // Cannot be 0 for std::min comparison m_minFps = AZ::Constants::FloatMax; @@ -93,13 +93,13 @@ namespace FPSProfiler void FPSProfilerSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { - if (m_configuration.m_ShowFPS) + if (m_configuration.m_ShowFps) { ShowFps(); } // Calculate data only if enabled, otherwise push default values to log entry. - if (m_configuration.m_SaveFPSData) + if (m_configuration.m_SaveFpsData) { CalculateFpsData(deltaTime); } diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp index 08616936..e778716d 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp @@ -15,10 +15,10 @@ namespace FPSProfiler ->Field("m_AutoSave", &FPSProfilerData::m_AutoSave) ->Field("m_AutoSaveOccurrences", &FPSProfilerData::m_AutoSaveOccurrences) ->Field("m_NearZeroPrecision", &FPSProfilerData::m_NearZeroPrecision) - ->Field("m_SaveFPSData", &FPSProfilerData::m_SaveFPSData) + ->Field("m_SaveFPSData", &FPSProfilerData::m_SaveFpsData) ->Field("m_SaveCPUData", &FPSProfilerData::m_SaveCPUData) ->Field("m_SaveGPUData", &FPSProfilerData::m_SaveGPUData) - ->Field("m_ShowFPS", &FPSProfilerData::m_ShowFPS); + ->Field("m_ShowFPS", &FPSProfilerData::m_ShowFps); if (AZ::EditContext* editContext = serializeContext->GetEditContext()) { @@ -75,7 +75,7 @@ namespace FPSProfiler ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerData::m_SaveFPSData, + &FPSProfilerData::m_SaveFpsData, "Save FPS Data", "When enabled, system will collect FPS data into csv.") @@ -95,7 +95,7 @@ namespace FPSProfiler ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerData::m_ShowFPS, + &FPSProfilerData::m_ShowFps, "Show FPS", "When enabled, system will show FPS counter in top-left corner."); } diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h index 1c9f7247..fe8d63d9 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h @@ -17,9 +17,9 @@ namespace FPSProfiler bool m_AutoSave = true; int m_AutoSaveOccurrences = 100; float m_NearZeroPrecision = 0.01f; - bool m_SaveFPSData = true; + bool m_SaveFpsData = true; bool m_SaveGPUData = true; bool m_SaveCPUData = true; - bool m_ShowFPS = true; + bool m_ShowFps = true; }; } // namespace FPSProfiler From a4b862f7ebb4549535e3b14c63d9cd543fe5352f Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 27 Feb 2025 13:28:31 +0100 Subject: [PATCH 035/175] move static functions to public Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.h | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 3bf8770f..87e399f0 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -55,13 +55,14 @@ namespace FPSProfiler // Helpers void CalculateFpsData(const float& deltaTime); - static float BytesToMB(size_t bytes); + // Debug display + void ShowFps() const; + + public: // Memory Access static size_t GetCpuMemoryUsed(); static size_t GetGpuMemoryUsed(); - - // Debug display - void ShowFps() const; + static float BytesToMB(size_t bytes); }; } // namespace FPSProfiler From 6a605622381a86a3b7aaa3709bfd1678d0845a32 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 27 Feb 2025 13:43:21 +0100 Subject: [PATCH 036/175] fix near zero editor reflection Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp index e778716d..e77ab7d6 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp @@ -70,6 +70,7 @@ namespace FPSProfiler "Specify near Zero precision, that will be used for system.") ->Attribute(AZ::Edit::Attributes::Min, 0.0f) ->Attribute(AZ::Edit::Attributes::Max, 0.1f) + ->Attribute(AZ::Edit::Attributes::Step, 0.00001f) ->ClassElement(AZ::Edit::ClassElements::Group, "Data Settings") From 6caedf3476e82993e1400a4bbd69b7c6cfaa35eb Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 27 Feb 2025 14:48:56 +0100 Subject: [PATCH 037/175] add request bus info | implement bus overrides Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 21 ++- .../Clients/FPSProfilerSystemComponent.cpp | 176 +++++++++++++----- .../Clients/FPSProfilerSystemComponent.h | 34 ++-- .../Code/Source/Tools/FPSProfilerData.cpp | 8 +- .../Code/Source/Tools/FPSProfilerData.h | 4 +- 5 files changed, 174 insertions(+), 69 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 00fdf29d..47c77bec 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -13,7 +13,26 @@ namespace FPSProfiler public: AZ_RTTI(FPSProfilerRequests, FPSProfilerRequestsTypeId); virtual ~FPSProfilerRequests() = default; - // Put your public methods here + + // Profiler control + virtual void StartProfiling() = 0; + virtual void StopProfiling() = 0; + virtual void ResetProfilingData() = 0; + virtual bool IsProfiling() const = 0; + + // Get Fps Data + virtual float GetMinFps() const = 0; + virtual float GetMaxFps() const = 0; + virtual float GetAvgFps() const = 0; + virtual float GetCurrentFps() const = 0; + + // Memory usage + virtual size_t GetCpuMemoryUsed() const = 0; + virtual size_t GetGpuMemoryUsed() const = 0; + + // Logging + virtual void SaveLogToFile() = 0; + virtual void ShowFpsOnScreen(bool enable) = 0; }; class FPSProfilerBusTraits : public AZ::EBusTraits diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index b938e692..8f4db747 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -69,15 +69,9 @@ namespace FPSProfiler m_logEntries.reserve(m_configuration.m_AutoSaveOccurrences * 2); } - if (m_configuration.m_SaveFpsData) - { - // Cannot be 0 for std::min comparison - m_minFps = AZ::Constants::FloatMax; - } - FPSProfilerRequestBus::Handler::BusConnect(); - AZ::TickBus::Handler::BusConnect(); CreateLogFile(); + StartProfiling(); } void FPSProfilerSystemComponent::Deactivate() @@ -87,33 +81,34 @@ namespace FPSProfiler // Notify - File Saved FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileSaved, m_configuration.m_OutputFilename.c_str()); - FPSProfilerRequestBus::Handler::BusDisconnect(); } void FPSProfilerSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { - if (m_configuration.m_ShowFps) + // Safety exit when profiling is disabled. + if (!m_isProfiling) { - ShowFps(); + return; } - // Calculate data only if enabled, otherwise push default values to log entry. - if (m_configuration.m_SaveFpsData) + CalculateFpsData(deltaTime); + + if (m_configuration.m_ShowFps) { - CalculateFpsData(deltaTime); + ShowFps(); } AZStd::string logEntry = AZStd::string::format( "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n", - m_frameCount, - deltaTime, - m_currentFps, - m_minFps, - m_maxFps, - m_avgFps, - m_configuration.m_SaveCPUData ? BytesToMB(GetCpuMemoryUsed()) : 0.0f, - m_configuration.m_SaveGPUData ? BytesToMB(GetGpuMemoryUsed()) : 0.0f); + m_configuration.m_SaveFpsData ? m_frameCount : -1, + m_configuration.m_SaveFpsData ? deltaTime : -1.0f, + m_configuration.m_SaveFpsData ? m_currentFps : -1.0f, + m_configuration.m_SaveFpsData ? m_minFps : -1.0f, + m_configuration.m_SaveFpsData ? m_maxFps : -1.0f, + m_configuration.m_SaveFpsData ? m_avgFps : -1.0f, + m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed()) : 0.0f, + m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed()) : 0.0f); m_logEntries.push_back(logEntry); // Save after every m_AutoSaveOccurrences frames to not overflow buffer, only when m_AutoSave enabled. @@ -128,6 +123,116 @@ namespace FPSProfiler return AZ::TICK_GAME; } + void FPSProfilerSystemComponent::StartProfiling() + { + if (m_isProfiling) + { + return; + } + + m_isProfiling = true; + ResetProfilingData(); + + if (!AZ::TickBus::Handler::BusIsConnected()) // Connect TickBus only if not already connected + { + AZ::TickBus::Handler::BusConnect(); + } + AZ_Printf("FPS Profiler", "Profiling started."); + } + + void FPSProfilerSystemComponent::StopProfiling() + { + if (!m_isProfiling) + { + return; + } + + m_isProfiling = false; + + if (AZ::TickBus::Handler::BusIsConnected()) // Only disconnect if actually connected + { + AZ::TickBus::Handler::BusDisconnect(); + } + SaveLogToFile(); + AZ_Printf("FPS Profiler", "Profiling stopped."); + } + + void FPSProfilerSystemComponent::ResetProfilingData() + { + m_minFps = AZ::Constants::FloatMax; + m_maxFps = 0.0f; + m_avgFps = 0.0f; + m_currentFps = 0.0f; + m_totalFrameTime = 0.0f; + m_frameCount = 0; + m_fpsSamples.clear(); + m_logEntries.clear(); + } + + bool FPSProfilerSystemComponent::IsProfiling() const + { + return m_isProfiling; + } + + float FPSProfilerSystemComponent::GetMinFps() const + { + return m_minFps; + } + + float FPSProfilerSystemComponent::GetMaxFps() const + { + return m_maxFps; + } + + float FPSProfilerSystemComponent::GetAvgFps() const + { + return m_avgFps; + } + + float FPSProfilerSystemComponent::GetCurrentFps() const + { + return m_currentFps; + } + + size_t FPSProfilerSystemComponent::GetCpuMemoryUsed() const + { + size_t usedBytes = 0; + size_t reservedBytes = 0; + + // Get stats for the system allocator + AZ::AllocatorManager::Instance().GetAllocatorStats(usedBytes, reservedBytes, nullptr); + + // Return the used bytes (allocated memory) + return usedBytes; + } + + size_t FPSProfilerSystemComponent::GetGpuMemoryUsed() const + { + if (AZ::RHI::RHISystemInterface* rhiSystem = AZ::RHI::RHISystemInterface::Get()) + { + if (AZ::RHI::Device* device = rhiSystem->GetDevice()) + { + AZ::RHI::MemoryStatistics memoryStats; + device->CompileMemoryStatistics(memoryStats, AZ::RHI::MemoryStatisticsReportFlags::Detail); + + // Return the GPU memory used in bytes + return memoryStats.m_heaps.front().m_memoryUsage.m_totalResidentInBytes; + } + } + + return 0; + } + + void FPSProfilerSystemComponent::SaveLogToFile() + { + WriteDataToFile(); + } + + void FPSProfilerSystemComponent::ShowFpsOnScreen(bool enable) + { + m_configuration.m_ShowFps = enable; + } + void FPSProfilerSystemComponent::CalculateFpsData(const float& deltaTime) { m_currentFps = deltaTime > 0 ? (1.0f / deltaTime) : 0.0f; @@ -211,35 +316,6 @@ namespace FPSProfiler FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileUpdate, m_configuration.m_OutputFilename.c_str()); } - size_t FPSProfilerSystemComponent::GetCpuMemoryUsed() - { - size_t usedBytes = 0; - size_t reservedBytes = 0; - - // Get stats for the system allocator - AZ::AllocatorManager::Instance().GetAllocatorStats(usedBytes, reservedBytes, nullptr); - - // Return the used bytes (allocated memory) - return usedBytes; - } - - size_t FPSProfilerSystemComponent::GetGpuMemoryUsed() - { - if (AZ::RHI::RHISystemInterface* rhiSystem = AZ::RHI::RHISystemInterface::Get()) - { - if (AZ::RHI::Device* device = rhiSystem->GetDevice()) - { - AZ::RHI::MemoryStatistics memoryStats; - device->CompileMemoryStatistics(memoryStats, AZ::RHI::MemoryStatisticsReportFlags::Detail); - - // Return the GPU memory used in bytes - return memoryStats.m_heaps.front().m_memoryUsage.m_totalResidentInBytes; - } - } - - return 0; - } - float FPSProfilerSystemComponent::BytesToMB(size_t bytes) { return static_cast(bytes) / (1024.0f * 1024.0f); diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 87e399f0..589be576 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -35,16 +35,31 @@ namespace FPSProfiler void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; int GetTickOrder() override; + // FPSProfilerRequestBus::Handler implementation + void StartProfiling() override; + void StopProfiling() override; + void ResetProfilingData() override; + bool IsProfiling() const override; + float GetMinFps() const override; + float GetMaxFps() const override; + float GetAvgFps() const override; + float GetCurrentFps() const override; + size_t GetCpuMemoryUsed() const override; + size_t GetGpuMemoryUsed() const override; + void SaveLogToFile() override; + void ShowFpsOnScreen(bool enable) override; + private: // Profiler Data - Editor Settings FPSProfilerData m_configuration; - float m_minFps = 0.0; // Tracking the lowest FPS value - float m_maxFps = 0.0f; // Tracking the highest FPS value - float m_avgFps = 0.0f; // Mean Value of accumulated current FPS - float m_currentFps = 0.0f; // Actual FPS in current frame - float m_totalFrameTime = 0.0f; // Time it took to enter frame - int m_frameCount = 0; // Numeric value of actual frame + bool m_isProfiling; + float m_minFps; // Tracking the lowest FPS value + float m_maxFps; // Tracking the highest FPS value + float m_avgFps; // Mean Value of accumulated current FPS + float m_currentFps; // Actual FPS in current frame + float m_totalFrameTime; // Time it took to enter frame + int m_frameCount; // Numeric value of actual frame AZStd::vector m_fpsSamples; // Vector of collected current FPSs. Cleared once @ref m_configuration.m_AutoSave enabled. AZStd::vector m_logEntries; // Vector of collected log entries. Cleared after @ref // m_configuration.m_AutoSaveOccurrences, when @ref m_configuration.m_AutoSave enabled. @@ -55,14 +70,9 @@ namespace FPSProfiler // Helpers void CalculateFpsData(const float& deltaTime); + static float BytesToMB(size_t bytes); // Debug display void ShowFps() const; - - public: - // Memory Access - static size_t GetCpuMemoryUsed(); - static size_t GetGpuMemoryUsed(); - static float BytesToMB(size_t bytes); }; } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp index e77ab7d6..3963fc1f 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp @@ -16,8 +16,8 @@ namespace FPSProfiler ->Field("m_AutoSaveOccurrences", &FPSProfilerData::m_AutoSaveOccurrences) ->Field("m_NearZeroPrecision", &FPSProfilerData::m_NearZeroPrecision) ->Field("m_SaveFPSData", &FPSProfilerData::m_SaveFpsData) - ->Field("m_SaveCPUData", &FPSProfilerData::m_SaveCPUData) - ->Field("m_SaveGPUData", &FPSProfilerData::m_SaveGPUData) + ->Field("m_SaveCPUData", &FPSProfilerData::m_SaveCpuData) + ->Field("m_SaveGPUData", &FPSProfilerData::m_SaveGpuData) ->Field("m_ShowFPS", &FPSProfilerData::m_ShowFps); if (AZ::EditContext* editContext = serializeContext->GetEditContext()) @@ -82,13 +82,13 @@ namespace FPSProfiler ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerData::m_SaveGPUData, + &FPSProfilerData::m_SaveGpuData, "Save GPU Data", "When enabled, system will collect GPU usage data into csv.") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerData::m_SaveCPUData, + &FPSProfilerData::m_SaveCpuData, "Save CPU Data", "When enabled, system will collect CPU usage data into csv.") diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h index fe8d63d9..3f279993 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h @@ -18,8 +18,8 @@ namespace FPSProfiler int m_AutoSaveOccurrences = 100; float m_NearZeroPrecision = 0.01f; bool m_SaveFpsData = true; - bool m_SaveGPUData = true; - bool m_SaveCPUData = true; + bool m_SaveGpuData = true; + bool m_SaveCpuData = true; bool m_ShowFps = true; }; } // namespace FPSProfiler From 632250f997a8a3628932ae89617e0c9a476acbdb Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 27 Feb 2025 14:55:38 +0100 Subject: [PATCH 038/175] fix activate / deactivate Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 4 +++- .../Code/Source/Clients/FPSProfilerSystemComponent.h | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 8f4db747..38d99639 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -70,8 +70,10 @@ namespace FPSProfiler } FPSProfilerRequestBus::Handler::BusConnect(); + CreateLogFile(); - StartProfiling(); + ResetProfilingData(); + AZ::TickBus::Handler::BusConnect(); } void FPSProfilerSystemComponent::Deactivate() diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 589be576..7b304803 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -50,9 +50,7 @@ namespace FPSProfiler void ShowFpsOnScreen(bool enable) override; private: - // Profiler Data - Editor Settings - FPSProfilerData m_configuration; - + FPSProfilerData m_configuration; // Profiler Data - Editor Settings bool m_isProfiling; float m_minFps; // Tracking the lowest FPS value float m_maxFps; // Tracking the highest FPS value From 523069971bba73dc57af3ca20b2f3199eee4174a Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 27 Feb 2025 15:01:59 +0100 Subject: [PATCH 039/175] rename profiler data -> profiler config Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 2 +- .../Clients/FPSProfilerSystemComponent.h | 9 ++-- ...ProfilerData.cpp => FPSProfilerConfig.cpp} | 46 +++++++++---------- ...{FPSProfilerData.h => FPSProfilerConfig.h} | 4 +- .../FPSProfilerEditorSystemComponent.cpp | 2 +- .../Tools/FPSProfilerEditorSystemComponent.h | 4 +- .../fpsprofiler_editor_private_files.cmake | 4 +- 7 files changed, 37 insertions(+), 34 deletions(-) rename Gems/FPSProfiler/Code/Source/Tools/{FPSProfilerData.cpp => FPSProfilerConfig.cpp} (68%) rename Gems/FPSProfiler/Code/Source/Tools/{FPSProfilerData.h => FPSProfilerConfig.h} (86%) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 38d99639..057fa0fe 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -36,7 +36,7 @@ namespace FPSProfiler } } - FPSProfilerSystemComponent::FPSProfilerSystemComponent(FPSProfilerData m_configuration) + FPSProfilerSystemComponent::FPSProfilerSystemComponent(FPSProfilerConfig m_configuration) : m_configuration(AZStd::move(m_configuration)) { if (FPSProfilerInterface::Get() == nullptr) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 7b304803..1811c664 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include #include @@ -23,7 +23,7 @@ namespace FPSProfiler static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); FPSProfilerSystemComponent(); - explicit FPSProfilerSystemComponent(FPSProfilerData m_configuration); + explicit FPSProfilerSystemComponent(FPSProfilerConfig m_configuration); ~FPSProfilerSystemComponent() override; protected: @@ -50,7 +50,10 @@ namespace FPSProfiler void ShowFpsOnScreen(bool enable) override; private: - FPSProfilerData m_configuration; // Profiler Data - Editor Settings + // Profiler Configuration - Editor Settings + FPSProfilerConfig m_configuration; + + // Profiler Data bool m_isProfiling; float m_minFps; // Tracking the lowest FPS value float m_maxFps; // Tracking the highest FPS value diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp similarity index 68% rename from Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp rename to Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index 3963fc1f..badc85e1 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -1,29 +1,29 @@ -#include "FPSProfilerData.h" +#include "FPSProfilerConfig.h" #include namespace FPSProfiler { - void FPSProfilerData::Reflect(AZ::ReflectContext* context) + void FPSProfilerConfig::Reflect(AZ::ReflectContext* context) { if (auto serializeContext = azrtti_cast(context)) { - serializeContext->Class() + serializeContext->Class() ->Version(0) - ->Field("m_OutputFilename", &FPSProfilerData::m_OutputFilename) - ->Field("m_SaveWithTimestamp", &FPSProfilerData::m_SaveWithTimestamp) - ->Field("m_AutoSave", &FPSProfilerData::m_AutoSave) - ->Field("m_AutoSaveOccurrences", &FPSProfilerData::m_AutoSaveOccurrences) - ->Field("m_NearZeroPrecision", &FPSProfilerData::m_NearZeroPrecision) - ->Field("m_SaveFPSData", &FPSProfilerData::m_SaveFpsData) - ->Field("m_SaveCPUData", &FPSProfilerData::m_SaveCpuData) - ->Field("m_SaveGPUData", &FPSProfilerData::m_SaveGpuData) - ->Field("m_ShowFPS", &FPSProfilerData::m_ShowFps); + ->Field("m_OutputFilename", &FPSProfilerConfig::m_OutputFilename) + ->Field("m_SaveWithTimestamp", &FPSProfilerConfig::m_SaveWithTimestamp) + ->Field("m_AutoSave", &FPSProfilerConfig::m_AutoSave) + ->Field("m_AutoSaveOccurrences", &FPSProfilerConfig::m_AutoSaveOccurrences) + ->Field("m_NearZeroPrecision", &FPSProfilerConfig::m_NearZeroPrecision) + ->Field("m_SaveFPSData", &FPSProfilerConfig::m_SaveFpsData) + ->Field("m_SaveCPUData", &FPSProfilerConfig::m_SaveCpuData) + ->Field("m_SaveGPUData", &FPSProfilerConfig::m_SaveGpuData) + ->Field("m_ShowFPS", &FPSProfilerConfig::m_ShowFps); if (AZ::EditContext* editContext = serializeContext->GetEditContext()) { editContext - ->Class("FPS Profiler Configuration", "Tracks FPS, GPU and CPU performance and saves it into .csv") + ->Class("FPS Profiler Configuration", "Tracks FPS, GPU and CPU performance and saves it into .csv") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::Category, "Performance") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) @@ -31,27 +31,27 @@ namespace FPSProfiler ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerData::m_OutputFilename, + &FPSProfilerConfig::m_OutputFilename, "Csv Save Path", "Select a path where *.csv will be saved.") ->Attribute(AZ::Edit::Attributes::SourceAssetFilterPattern, "*.csv") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerData::m_SaveWithTimestamp, + &FPSProfilerConfig::m_SaveWithTimestamp, "Timestamp", "When enabled, system will save files with timestamp postfix of current date and hour.") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerData::m_AutoSave, + &FPSProfilerConfig::m_AutoSave, "Auto Save", "When enabled, system will auto save after specified frame occurrance.") ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree) ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerData::m_AutoSaveOccurrences, + &FPSProfilerConfig::m_AutoSaveOccurrences, "Auto Save At Frame", "Specify after how many frames system will auto save log.") ->Attribute(AZ::Edit::Attributes::Min, 1) @@ -59,13 +59,13 @@ namespace FPSProfiler AZ::Edit::Attributes::Visibility, [](const void* instance) { - const FPSProfilerData* data = reinterpret_cast(instance); + const FPSProfilerConfig* data = reinterpret_cast(instance); return data && data->m_AutoSave ? AZ::Edit::PropertyVisibility::Show : AZ::Edit::PropertyVisibility::Hide; }) ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerData::m_NearZeroPrecision, + &FPSProfilerConfig::m_NearZeroPrecision, "Near Zero Precision", "Specify near Zero precision, that will be used for system.") ->Attribute(AZ::Edit::Attributes::Min, 0.0f) @@ -76,19 +76,19 @@ namespace FPSProfiler ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerData::m_SaveFpsData, + &FPSProfilerConfig::m_SaveFpsData, "Save FPS Data", "When enabled, system will collect FPS data into csv.") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerData::m_SaveGpuData, + &FPSProfilerConfig::m_SaveGpuData, "Save GPU Data", "When enabled, system will collect GPU usage data into csv.") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerData::m_SaveCpuData, + &FPSProfilerConfig::m_SaveCpuData, "Save CPU Data", "When enabled, system will collect CPU usage data into csv.") @@ -96,7 +96,7 @@ namespace FPSProfiler ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerData::m_ShowFps, + &FPSProfilerConfig::m_ShowFps, "Show FPS", "When enabled, system will show FPS counter in top-left corner."); } diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h similarity index 86% rename from Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h rename to Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h index 3f279993..cc6ea213 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerData.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h @@ -7,9 +7,9 @@ namespace FPSProfiler { - struct FPSProfilerData + struct FPSProfilerConfig { - AZ_TYPE_INFO(FPSProfilerData, FPSProfilerDataTypeId); + AZ_TYPE_INFO(FPSProfilerConfig, FPSProfilerDataTypeId); static void Reflect(AZ::ReflectContext* context); AZ::IO::Path m_OutputFilename = "@user@/fps_log.csv"; diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 5d786af8..266f4745 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -7,7 +7,7 @@ namespace FPSProfiler { void FPSProfilerEditorSystemComponent::Reflect(AZ::ReflectContext* context) { - FPSProfilerData::Reflect(context); + FPSProfilerConfig::Reflect(context); if (auto serializeContext = azrtti_cast(context)) { diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h index aac53fe5..8e3a51e9 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h @@ -1,6 +1,6 @@ #pragma once -#include "FPSProfilerData.h" +#include "FPSProfilerConfig.h" #include #include @@ -30,6 +30,6 @@ namespace FPSProfiler static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); - FPSProfilerData m_configuration; + FPSProfilerConfig m_configuration; }; } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake index 1c4c33b6..716f2d65 100644 --- a/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake +++ b/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake @@ -1,7 +1,7 @@ set(FILES - Source/Tools/FPSProfilerData.cpp - Source/Tools/FPSProfilerData.h + Source/Tools/FPSProfilerConfig.cpp + Source/Tools/FPSProfilerConfig.h Source/Tools/FPSProfilerEditorSystemComponent.cpp Source/Tools/FPSProfilerEditorSystemComponent.h ) From a1f031cc60b36c2eb91f88cccbb0027f54bae314 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 27 Feb 2025 15:06:07 +0100 Subject: [PATCH 040/175] add fix config layout Signed-off-by: Wojciech Czerski --- .../Code/Source/Tools/FPSProfilerConfig.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index badc85e1..8ac1ac5e 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -36,12 +36,6 @@ namespace FPSProfiler "Select a path where *.csv will be saved.") ->Attribute(AZ::Edit::Attributes::SourceAssetFilterPattern, "*.csv") - ->DataElement( - AZ::Edit::UIHandlers::Default, - &FPSProfilerConfig::m_SaveWithTimestamp, - "Timestamp", - "When enabled, system will save files with timestamp postfix of current date and hour.") - ->DataElement( AZ::Edit::UIHandlers::Default, &FPSProfilerConfig::m_AutoSave, @@ -63,6 +57,14 @@ namespace FPSProfiler return data && data->m_AutoSave ? AZ::Edit::PropertyVisibility::Show : AZ::Edit::PropertyVisibility::Hide; }) + ->DataElement( + AZ::Edit::UIHandlers::Default, + &FPSProfilerConfig::m_SaveWithTimestamp, + "Timestamp", + "When enabled, system will save files with timestamp postfix of current date and hour.") + + ->ClassElement(AZ::Edit::ClassElements::Group, "Precision Settings") + ->DataElement( AZ::Edit::UIHandlers::Default, &FPSProfilerConfig::m_NearZeroPrecision, From 18444cccc3c3235f93d99a5bbc15aa38170fbabf Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 27 Feb 2025 15:07:34 +0100 Subject: [PATCH 041/175] add fix config layout Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp | 2 +- Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index 8ac1ac5e..aa66f870 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -11,9 +11,9 @@ namespace FPSProfiler serializeContext->Class() ->Version(0) ->Field("m_OutputFilename", &FPSProfilerConfig::m_OutputFilename) - ->Field("m_SaveWithTimestamp", &FPSProfilerConfig::m_SaveWithTimestamp) ->Field("m_AutoSave", &FPSProfilerConfig::m_AutoSave) ->Field("m_AutoSaveOccurrences", &FPSProfilerConfig::m_AutoSaveOccurrences) + ->Field("m_SaveWithTimestamp", &FPSProfilerConfig::m_SaveWithTimestamp) ->Field("m_NearZeroPrecision", &FPSProfilerConfig::m_NearZeroPrecision) ->Field("m_SaveFPSData", &FPSProfilerConfig::m_SaveFpsData) ->Field("m_SaveCPUData", &FPSProfilerConfig::m_SaveCpuData) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h index cc6ea213..ccc82aa2 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h @@ -13,9 +13,9 @@ namespace FPSProfiler static void Reflect(AZ::ReflectContext* context); AZ::IO::Path m_OutputFilename = "@user@/fps_log.csv"; - bool m_SaveWithTimestamp = true; bool m_AutoSave = true; int m_AutoSaveOccurrences = 100; + bool m_SaveWithTimestamp = true; float m_NearZeroPrecision = 0.01f; bool m_SaveFpsData = true; bool m_SaveGpuData = true; From e1e25a91155aa9f3a23765dbbac102f9ec823128 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 27 Feb 2025 15:21:43 +0100 Subject: [PATCH 042/175] add is any save option enabled Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 1 + .../Clients/FPSProfilerSystemComponent.cpp | 17 +++++++++++++++++ .../Source/Clients/FPSProfilerSystemComponent.h | 1 + 3 files changed, 19 insertions(+) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 47c77bec..2a66e84c 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -19,6 +19,7 @@ namespace FPSProfiler virtual void StopProfiling() = 0; virtual void ResetProfilingData() = 0; virtual bool IsProfiling() const = 0; + virtual bool IsAnySaveOptionEnabled() const = 0; // Get Fps Data virtual float GetMinFps() const = 0; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 057fa0fe..f22175b9 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -61,6 +61,12 @@ namespace FPSProfiler return; } + // If none save option enabled - exit + if (!IsAnySaveOptionEnabled()) + { + return; + } + // Reserve at least twice as needed occurrences, since close and save operation may happen at the tick frame saves. // Since log entries are cleared when occurrence update happens, it's good to reserve known size. if (m_configuration.m_AutoSave) @@ -94,6 +100,12 @@ namespace FPSProfiler return; } + // If none save option enabled - exit + if (!IsAnySaveOptionEnabled()) + { + return; + } + CalculateFpsData(deltaTime); if (m_configuration.m_ShowFps) @@ -176,6 +188,11 @@ namespace FPSProfiler return m_isProfiling; } + bool FPSProfilerSystemComponent::IsAnySaveOptionEnabled() const + { + return m_configuration.m_SaveFpsData || m_configuration.m_SaveCpuData || m_configuration.m_SaveGpuData; + } + float FPSProfilerSystemComponent::GetMinFps() const { return m_minFps; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 1811c664..feaa3028 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -40,6 +40,7 @@ namespace FPSProfiler void StopProfiling() override; void ResetProfilingData() override; bool IsProfiling() const override; + bool IsAnySaveOptionEnabled() const override; float GetMinFps() const override; float GetMaxFps() const override; float GetAvgFps() const override; From 9e53459eaa1d4afbe5cae2a346da7054597aa7bc Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 27 Feb 2025 16:53:23 +0100 Subject: [PATCH 043/175] add option to change save path Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 3 ++ .../Clients/FPSProfilerSystemComponent.cpp | 28 +++++++++++++++++++ .../Clients/FPSProfilerSystemComponent.h | 3 ++ 3 files changed, 34 insertions(+) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 2a66e84c..6beedfe2 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -20,6 +20,8 @@ namespace FPSProfiler virtual void ResetProfilingData() = 0; virtual bool IsProfiling() const = 0; virtual bool IsAnySaveOptionEnabled() const = 0; + virtual void ChangeSavePath(const AZStd::string& newSavePath) = 0; + virtual void SafeChangeSavePath(const AZStd::string& newSavePath) = 0; // Get Fps Data virtual float GetMinFps() const = 0; @@ -33,6 +35,7 @@ namespace FPSProfiler // Logging virtual void SaveLogToFile() = 0; + virtual void SaveLogToFile(const AZStd::string& newSavePath, bool useSafeChangePath=true) = 0; virtual void ShowFpsOnScreen(bool enable) = 0; }; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index f22175b9..ad2d4568 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -106,6 +106,7 @@ namespace FPSProfiler return; } + // Calculate data for Profiler Bus CalculateFpsData(deltaTime); if (m_configuration.m_ShowFps) @@ -193,6 +194,19 @@ namespace FPSProfiler return m_configuration.m_SaveFpsData || m_configuration.m_SaveCpuData || m_configuration.m_SaveGpuData; } + void FPSProfilerSystemComponent::ChangeSavePath(const AZStd::string& newSavePath) + { + AZ_Printf("FPS Profiler", "Path changed."); + m_configuration.m_OutputFilename = newSavePath; + } + + void FPSProfilerSystemComponent::SafeChangeSavePath(const AZStd::string& newSavePath) + { + // If profiling is enabled, save current open file and stop profiling. + StopProfiling(); + ChangeSavePath(newSavePath); + } + float FPSProfilerSystemComponent::GetMinFps() const { return m_minFps; @@ -247,6 +261,20 @@ namespace FPSProfiler WriteDataToFile(); } + void FPSProfilerSystemComponent::SaveLogToFile(const AZStd::string& newSavePath, bool useSafeChangePath) + { + if (useSafeChangePath) + { + SafeChangeSavePath(newSavePath); + } + else + { + ChangeSavePath(newSavePath); + } + + WriteDataToFile(); + } + void FPSProfilerSystemComponent::ShowFpsOnScreen(bool enable) { m_configuration.m_ShowFps = enable; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index feaa3028..e25667cd 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -41,6 +41,8 @@ namespace FPSProfiler void ResetProfilingData() override; bool IsProfiling() const override; bool IsAnySaveOptionEnabled() const override; + void ChangeSavePath(const AZStd::string& newSavePath) override; + void SafeChangeSavePath(const AZStd::string& newSavePath) override; float GetMinFps() const override; float GetMaxFps() const override; float GetAvgFps() const override; @@ -48,6 +50,7 @@ namespace FPSProfiler size_t GetCpuMemoryUsed() const override; size_t GetGpuMemoryUsed() const override; void SaveLogToFile() override; + void SaveLogToFile(const AZStd::string& newSavePath, bool useSafeChangePath) override; void ShowFpsOnScreen(bool enable) override; private: From 2aa4a313499b22335ee0a1f2439015b12dac36da Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 27 Feb 2025 16:55:35 +0100 Subject: [PATCH 044/175] clang format Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 6beedfe2..41db0947 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -35,7 +35,7 @@ namespace FPSProfiler // Logging virtual void SaveLogToFile() = 0; - virtual void SaveLogToFile(const AZStd::string& newSavePath, bool useSafeChangePath=true) = 0; + virtual void SaveLogToFile(const AZStd::string& newSavePath, bool useSafeChangePath = true) = 0; virtual void ShowFpsOnScreen(bool enable) = 0; }; From 3f34943d7edf7438934bee38d51e642ddad32ec3 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 27 Feb 2025 16:56:24 +0100 Subject: [PATCH 045/175] typo fix Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index ad2d4568..18005334 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -202,7 +202,7 @@ namespace FPSProfiler void FPSProfilerSystemComponent::SafeChangeSavePath(const AZStd::string& newSavePath) { - // If profiling is enabled, save current open file and stop profiling. + // If profiling is enabled, save current opened file and stop profiling. StopProfiling(); ChangeSavePath(newSavePath); } From 48173da573af31ab1b70dee05b4bc6926bdb45ee Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 27 Feb 2025 16:58:48 +0100 Subject: [PATCH 046/175] exit early when no save option enabled Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 18005334..1209da5c 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -349,6 +349,12 @@ namespace FPSProfiler return; } + // If none save option enabled - exit + if (!IsAnySaveOptionEnabled()) + { + return; + } + AZ::IO::FileIOStream file(m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeAppend); for (const auto& entry : m_logEntries) From 53f4144e9ff4f2fa60e508a6180a87ad4da63c0e Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Fri, 28 Feb 2025 13:33:15 +0100 Subject: [PATCH 047/175] mark disabled data to -1.0f | using 0.0f might be confusing Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 1209da5c..9b03d852 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -122,8 +122,8 @@ namespace FPSProfiler m_configuration.m_SaveFpsData ? m_minFps : -1.0f, m_configuration.m_SaveFpsData ? m_maxFps : -1.0f, m_configuration.m_SaveFpsData ? m_avgFps : -1.0f, - m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed()) : 0.0f, - m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed()) : 0.0f); + m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed()) : -1.0f, + m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed()) : -1.0f); m_logEntries.push_back(logEntry); // Save after every m_AutoSaveOccurrences frames to not overflow buffer, only when m_AutoSave enabled. From 0cd0d2c82948e99f40b557ec989779bc64eb3db9 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Fri, 28 Feb 2025 13:48:48 +0100 Subject: [PATCH 048/175] fix near zero comparison Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 9b03d852..3ca006e1 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -289,7 +289,7 @@ namespace FPSProfiler m_frameCount++; // Using m_NearZeroPrecision, since m_currentFPS cannot be equal to 0 if delta time is valid. - if (m_currentFps > m_configuration.m_NearZeroPrecision) + if (m_currentFps >= m_configuration.m_NearZeroPrecision) { m_minFps = AZStd::min(m_minFps, m_currentFps); } From 790e65ebd9d96a9d04b8398cd3981c614ab56377 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Fri, 28 Feb 2025 14:02:28 +0100 Subject: [PATCH 049/175] add option to disable instant profiling in editor Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 16 ++++++++-------- .../Source/Clients/FPSProfilerSystemComponent.h | 2 +- .../Tools/FPSProfilerEditorSystemComponent.cpp | 15 +++++++++++---- .../Tools/FPSProfilerEditorSystemComponent.h | 1 + 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 3ca006e1..b967ce21 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -36,8 +36,8 @@ namespace FPSProfiler } } - FPSProfilerSystemComponent::FPSProfilerSystemComponent(FPSProfilerConfig m_configuration) - : m_configuration(AZStd::move(m_configuration)) + FPSProfilerSystemComponent::FPSProfilerSystemComponent(FPSProfilerConfig m_configuration, bool m_profileOnGameStart) + : m_configuration(AZStd::move(m_configuration)), m_isProfiling(m_profileOnGameStart) { if (FPSProfilerInterface::Get() == nullptr) { @@ -100,12 +100,6 @@ namespace FPSProfiler return; } - // If none save option enabled - exit - if (!IsAnySaveOptionEnabled()) - { - return; - } - // Calculate data for Profiler Bus CalculateFpsData(deltaTime); @@ -114,6 +108,12 @@ namespace FPSProfiler ShowFps(); } + // If none save option enabled - exit + if (!IsAnySaveOptionEnabled()) + { + return; + } + AZStd::string logEntry = AZStd::string::format( "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n", m_configuration.m_SaveFpsData ? m_frameCount : -1, diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index e25667cd..dd1e1156 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -23,7 +23,7 @@ namespace FPSProfiler static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); FPSProfilerSystemComponent(); - explicit FPSProfilerSystemComponent(FPSProfilerConfig m_configuration); + explicit FPSProfilerSystemComponent(FPSProfilerConfig m_configuration, bool m_profileOnGameStart); ~FPSProfilerSystemComponent() override; protected: diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 266f4745..0ce05142 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -11,8 +11,10 @@ namespace FPSProfiler if (auto serializeContext = azrtti_cast(context)) { - serializeContext->Class()->Version(0)->Field( - "m_Configuration", &FPSProfilerEditorSystemComponent::m_configuration); + serializeContext->Class() + ->Version(0) + ->Field("m_Configuration", &FPSProfilerEditorSystemComponent::m_configuration) + ->Field("m_profileOnGameStart", &FPSProfilerEditorSystemComponent::m_profileOnGameStart); if (AZ::EditContext* editContext = serializeContext->GetEditContext()) { @@ -22,7 +24,12 @@ namespace FPSProfiler ->Attribute(AZ::Edit::Attributes::Category, "Performance") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) ->Attribute(AZ::Edit::Attributes::AutoExpand, true) - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configuration); + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configuration) + + ->DataElement(AZ::Edit::UIHandlers::Default, + &FPSProfilerEditorSystemComponent::m_profileOnGameStart, + "Profile On Game Start", + "Should system start profiling data instantly after game is launched, or await for other system to activate it?"); } } } @@ -49,6 +56,6 @@ namespace FPSProfiler void FPSProfilerEditorSystemComponent::BuildGameEntity(AZ::Entity* entity) { - entity->CreateComponent(m_configuration); + entity->CreateComponent(m_configuration, m_profileOnGameStart); } } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h index 8e3a51e9..9e024a86 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h @@ -31,5 +31,6 @@ namespace FPSProfiler static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); FPSProfilerConfig m_configuration; + bool m_profileOnGameStart = true; }; } // namespace FPSProfiler From 63aab4f6cfe7fbcd1da5a2fb15b5da9f5551582b Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Fri, 28 Feb 2025 14:35:34 +0100 Subject: [PATCH 050/175] fix profiling setup on tick and activate Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 20 ++++++++++++------- .../Tools/FPSProfilerEditorSystemComponent.h | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index b967ce21..455953b5 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -61,11 +61,22 @@ namespace FPSProfiler return; } - // If none save option enabled - exit + // If none, dont proceed + if (!(m_isProfiling && m_configuration.m_ShowFps)) + { + return; + } + + FPSProfilerRequestBus::Handler::BusConnect(); // connect first to broadcast notifications + ResetProfilingData(); + AZ::TickBus::Handler::BusConnect(); // connect last, after setup + AZ_Printf("FPS Profiler", "Activating FPSProfiler"); + if (!IsAnySaveOptionEnabled()) { return; } + CreateLogFile(); // Reserve at least twice as needed occurrences, since close and save operation may happen at the tick frame saves. // Since log entries are cleared when occurrence update happens, it's good to reserve known size. @@ -74,12 +85,6 @@ namespace FPSProfiler m_fpsSamples.reserve(m_configuration.m_AutoSaveOccurrences * 2); m_logEntries.reserve(m_configuration.m_AutoSaveOccurrences * 2); } - - FPSProfilerRequestBus::Handler::BusConnect(); - - CreateLogFile(); - ResetProfilingData(); - AZ::TickBus::Handler::BusConnect(); } void FPSProfilerSystemComponent::Deactivate() @@ -147,6 +152,7 @@ namespace FPSProfiler m_isProfiling = true; ResetProfilingData(); + CreateLogFile(); if (!AZ::TickBus::Handler::BusIsConnected()) // Connect TickBus only if not already connected { diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h index 9e024a86..af7299cd 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h @@ -31,6 +31,6 @@ namespace FPSProfiler static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); FPSProfilerConfig m_configuration; - bool m_profileOnGameStart = true; + bool m_profileOnGameStart{}; }; } // namespace FPSProfiler From e6e50134c55df0fc06014f625dea6110d7c0c1c1 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Fri, 28 Feb 2025 15:38:34 +0100 Subject: [PATCH 051/175] clang format Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 3 ++- .../Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 455953b5..b01e519d 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -37,7 +37,8 @@ namespace FPSProfiler } FPSProfilerSystemComponent::FPSProfilerSystemComponent(FPSProfilerConfig m_configuration, bool m_profileOnGameStart) - : m_configuration(AZStd::move(m_configuration)), m_isProfiling(m_profileOnGameStart) + : m_configuration(AZStd::move(m_configuration)) + , m_isProfiling(m_profileOnGameStart) { if (FPSProfilerInterface::Get() == nullptr) { diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 0ce05142..432f24c6 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -26,7 +26,8 @@ namespace FPSProfiler ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configuration) - ->DataElement(AZ::Edit::UIHandlers::Default, + ->DataElement( + AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_profileOnGameStart, "Profile On Game Start", "Should system start profiling data instantly after game is launched, or await for other system to activate it?"); From efad6b9d18ec1ee26e106b9bd7a67ce24b02fb5e Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 3 Mar 2025 11:45:09 +0100 Subject: [PATCH 052/175] apply code review | add path validation | code improvement Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 9 +- .../Clients/FPSProfilerSystemComponent.cpp | 88 +++++++++++++------ .../Clients/FPSProfilerSystemComponent.h | 21 ++--- .../Code/Source/Tools/FPSProfilerConfig.cpp | 4 +- .../Code/Source/Tools/FPSProfilerConfig.h | 2 +- .../FPSProfilerEditorSystemComponent.cpp | 2 +- 6 files changed, 80 insertions(+), 46 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 41db0947..e20b7d91 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -20,8 +21,10 @@ namespace FPSProfiler virtual void ResetProfilingData() = 0; virtual bool IsProfiling() const = 0; virtual bool IsAnySaveOptionEnabled() const = 0; - virtual void ChangeSavePath(const AZStd::string& newSavePath) = 0; - virtual void SafeChangeSavePath(const AZStd::string& newSavePath) = 0; + virtual void ChangeSavePath( + const AZ::IO::Path& newSavePath) = 0; //!< Caution! This function is not runtime safe. Instead use @ref SafeChangeSavePath + virtual void SafeChangeSavePath(const AZ::IO::Path& newSavePath) = 0; //!< Runtime safe path changing. Saves and stops current + //!< profiling and changes path afterwards. // Get Fps Data virtual float GetMinFps() const = 0; @@ -35,7 +38,7 @@ namespace FPSProfiler // Logging virtual void SaveLogToFile() = 0; - virtual void SaveLogToFile(const AZStd::string& newSavePath, bool useSafeChangePath = true) = 0; + virtual void SaveLogToFileWithNewPath(const AZStd::string& newSavePath, bool useSafeChangePath) = 0; virtual void ShowFpsOnScreen(bool enable) = 0; }; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index b01e519d..80a26ed0 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -58,14 +58,8 @@ namespace FPSProfiler { if (m_configuration.m_OutputFilename.empty()) { - AZ_Error("FPSProfiler", false, "The output filename must be provided or cannot be empty!"); - return; - } - - // If none, dont proceed - if (!(m_isProfiling && m_configuration.m_ShowFps)) - { - return; + m_configuration.m_OutputFilename = "@user@/fps_log.csv"; + AZ_Warning("FPSProfiler", false, "Invalid output file path. Using default: %s", m_configuration.m_OutputFilename.c_str()); } FPSProfilerRequestBus::Handler::BusConnect(); // connect first to broadcast notifications @@ -73,18 +67,17 @@ namespace FPSProfiler AZ::TickBus::Handler::BusConnect(); // connect last, after setup AZ_Printf("FPS Profiler", "Activating FPSProfiler"); - if (!IsAnySaveOptionEnabled()) + if (IsAnySaveOptionEnabled()) { - return; + CreateLogFile(); } - CreateLogFile(); // Reserve at least twice as needed occurrences, since close and save operation may happen at the tick frame saves. // Since log entries are cleared when occurrence update happens, it's good to reserve known size. if (m_configuration.m_AutoSave) { - m_fpsSamples.reserve(m_configuration.m_AutoSaveOccurrences * 2); - m_logEntries.reserve(m_configuration.m_AutoSaveOccurrences * 2); + m_fpsSamples.reserve(m_configuration.m_AutoSaveAtFrame * 2); + m_logEntries.reserve(m_configuration.m_AutoSaveAtFrame * 2); } } @@ -133,7 +126,7 @@ namespace FPSProfiler m_logEntries.push_back(logEntry); // Save after every m_AutoSaveOccurrences frames to not overflow buffer, only when m_AutoSave enabled. - if (m_configuration.m_AutoSave && m_frameCount % m_configuration.m_AutoSaveOccurrences == 0) + if (m_configuration.m_AutoSave && m_frameCount % m_configuration.m_AutoSaveAtFrame == 0) { WriteDataToFile(); } @@ -201,13 +194,18 @@ namespace FPSProfiler return m_configuration.m_SaveFpsData || m_configuration.m_SaveCpuData || m_configuration.m_SaveGpuData; } - void FPSProfilerSystemComponent::ChangeSavePath(const AZStd::string& newSavePath) + void FPSProfilerSystemComponent::ChangeSavePath(const AZ::IO::Path& newSavePath) { - AZ_Printf("FPS Profiler", "Path changed."); + if (!IsPathValid(newSavePath)) + { + return; + } + m_configuration.m_OutputFilename = newSavePath; + AZ_Warning("FPS Profiler", false, "Path changed."); } - void FPSProfilerSystemComponent::SafeChangeSavePath(const AZStd::string& newSavePath) + void FPSProfilerSystemComponent::SafeChangeSavePath(const AZ::IO::Path& newSavePath) { // If profiling is enabled, save current opened file and stop profiling. StopProfiling(); @@ -268,7 +266,7 @@ namespace FPSProfiler WriteDataToFile(); } - void FPSProfilerSystemComponent::SaveLogToFile(const AZStd::string& newSavePath, bool useSafeChangePath) + void FPSProfilerSystemComponent::SaveLogToFileWithNewPath(const AZStd::string& newSavePath, bool useSafeChangePath) { if (useSafeChangePath) { @@ -307,17 +305,32 @@ namespace FPSProfiler void FPSProfilerSystemComponent::CreateLogFile() { - AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); - bool fileExists = fileIO->Exists(m_configuration.m_OutputFilename.c_str()); + if (!IsPathValid(m_configuration.m_OutputFilename)) + { + m_configuration.m_OutputFilename = "@user@/fps_log.csv"; + AZ_Warning("FPSProfiler", false, "Invalid output file path. Using default: %s", m_configuration.m_OutputFilename.c_str()); + return; + } - if (!fileExists) + AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); + if (!fileIO->Exists(m_configuration.m_OutputFilename.c_str())) { - m_configuration.m_OutputFilename = "@user@/fps_log.csv"; // Restore to default path - AZ_Error( - "FPSProfiler::CreateLogFile", - false, - "Specified file does not exist. Using default path: %s", - m_configuration.m_OutputFilename.c_str()); + AZ_Warning("FPSProfiler", false, "File does not exist, trying to create it..."); + + AZ::IO::HandleType fileHandle; + if (fileIO->Open( + m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath, fileHandle) == + AZ::IO::ResultCode::Success) + { + fileIO->Close(fileHandle); + AZ_Printf("FPSProfiler", "Log file successfully created: %s", m_configuration.m_OutputFilename.c_str()); + } + else + { + // Restore default path on fail + m_configuration.m_OutputFilename = "@user@/fps_log.csv"; + AZ_Warning("FPSProfiler", false, "Failed to create file. Using default path: %s", m_configuration.m_OutputFilename.c_str()); + } } if (m_configuration.m_SaveWithTimestamp) @@ -350,13 +363,11 @@ namespace FPSProfiler void FPSProfilerSystemComponent::WriteDataToFile() { - // Exit when nothing to save if (m_logEntries.empty()) { return; } - // If none save option enabled - exit if (!IsAnySaveOptionEnabled()) { return; @@ -381,6 +392,25 @@ namespace FPSProfiler return static_cast(bytes) / (1024.0f * 1024.0f); } + bool FPSProfilerSystemComponent::IsPathValid(const AZ::IO::Path& path) + { + AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); + + if (path.empty() || !path.HasFilename() || !path.HasExtension() || !fileIO || !fileIO->ResolvePath(path.c_str())) + { + const char* reason = path.empty() ? "Path cannot be empty." + : !path.HasFilename() ? "Path must have a file at the end." + : !path.HasExtension() ? "Path must have a *.csv extension." + : !fileIO ? "Could not get a FileIO object. Try again." + : "Path is not registered or recognizable by O3DE FileIO System."; + + AZ_Warning("FPSProfiler::ChangeSavePath", false, "%s", reason); + return false; + } + + return true; + } + void FPSProfilerSystemComponent::ShowFps() const { AzFramework::DebugDisplayRequestBus::BusPtr debugDisplayBus; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index dd1e1156..1c205e19 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -41,8 +41,8 @@ namespace FPSProfiler void ResetProfilingData() override; bool IsProfiling() const override; bool IsAnySaveOptionEnabled() const override; - void ChangeSavePath(const AZStd::string& newSavePath) override; - void SafeChangeSavePath(const AZStd::string& newSavePath) override; + void ChangeSavePath(const AZ::IO::Path& newSavePath) override; + void SafeChangeSavePath(const AZ::IO::Path& newSavePath) override; float GetMinFps() const override; float GetMaxFps() const override; float GetAvgFps() const override; @@ -50,7 +50,7 @@ namespace FPSProfiler size_t GetCpuMemoryUsed() const override; size_t GetGpuMemoryUsed() const override; void SaveLogToFile() override; - void SaveLogToFile(const AZStd::string& newSavePath, bool useSafeChangePath) override; + void SaveLogToFileWithNewPath(const AZStd::string& newSavePath, bool useSafeChangePath) override; void ShowFpsOnScreen(bool enable) override; private: @@ -58,13 +58,13 @@ namespace FPSProfiler FPSProfilerConfig m_configuration; // Profiler Data - bool m_isProfiling; - float m_minFps; // Tracking the lowest FPS value - float m_maxFps; // Tracking the highest FPS value - float m_avgFps; // Mean Value of accumulated current FPS - float m_currentFps; // Actual FPS in current frame - float m_totalFrameTime; // Time it took to enter frame - int m_frameCount; // Numeric value of actual frame + bool m_isProfiling = false; + float m_minFps = 0.0f; // Tracking the lowest FPS value + float m_maxFps = 0.0f; // Tracking the highest FPS value + float m_avgFps = 0.0f; // Mean Value of accumulated current FPS + float m_currentFps = 0.0f; // Actual FPS in current frame + float m_totalFrameTime = 0.0f; // Time it took to enter frame + int m_frameCount = 0; // Numeric value of actual frame AZStd::vector m_fpsSamples; // Vector of collected current FPSs. Cleared once @ref m_configuration.m_AutoSave enabled. AZStd::vector m_logEntries; // Vector of collected log entries. Cleared after @ref // m_configuration.m_AutoSaveOccurrences, when @ref m_configuration.m_AutoSave enabled. @@ -76,6 +76,7 @@ namespace FPSProfiler // Helpers void CalculateFpsData(const float& deltaTime); static float BytesToMB(size_t bytes); + static bool IsPathValid(const AZ::IO::Path& path); // Debug display void ShowFps() const; diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index aa66f870..19f78498 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -12,7 +12,7 @@ namespace FPSProfiler ->Version(0) ->Field("m_OutputFilename", &FPSProfilerConfig::m_OutputFilename) ->Field("m_AutoSave", &FPSProfilerConfig::m_AutoSave) - ->Field("m_AutoSaveOccurrences", &FPSProfilerConfig::m_AutoSaveOccurrences) + ->Field("m_AutoSaveOccurrences", &FPSProfilerConfig::m_AutoSaveAtFrame) ->Field("m_SaveWithTimestamp", &FPSProfilerConfig::m_SaveWithTimestamp) ->Field("m_NearZeroPrecision", &FPSProfilerConfig::m_NearZeroPrecision) ->Field("m_SaveFPSData", &FPSProfilerConfig::m_SaveFpsData) @@ -45,7 +45,7 @@ namespace FPSProfiler ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfig::m_AutoSaveOccurrences, + &FPSProfilerConfig::m_AutoSaveAtFrame, "Auto Save At Frame", "Specify after how many frames system will auto save log.") ->Attribute(AZ::Edit::Attributes::Min, 1) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h index ccc82aa2..1c3b4634 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h @@ -14,7 +14,7 @@ namespace FPSProfiler AZ::IO::Path m_OutputFilename = "@user@/fps_log.csv"; bool m_AutoSave = true; - int m_AutoSaveOccurrences = 100; + int m_AutoSaveAtFrame = 100; bool m_SaveWithTimestamp = true; float m_NearZeroPrecision = 0.01f; bool m_SaveFpsData = true; diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 432f24c6..3ce35673 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -11,7 +11,7 @@ namespace FPSProfiler if (auto serializeContext = azrtti_cast(context)) { - serializeContext->Class() + serializeContext->Class() ->Version(0) ->Field("m_Configuration", &FPSProfilerEditorSystemComponent::m_configuration) ->Field("m_profileOnGameStart", &FPSProfilerEditorSystemComponent::m_profileOnGameStart); From d3c67c82aff80f8c3bad3202ac98adfa8db22cae Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 3 Mar 2025 11:49:02 +0100 Subject: [PATCH 053/175] clang tidy & format Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 20 +++++++++---------- .../Clients/FPSProfilerSystemComponent.cpp | 6 ++++-- .../Clients/FPSProfilerSystemComponent.h | 16 +++++++-------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index e20b7d91..76f3b1db 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -19,22 +19,22 @@ namespace FPSProfiler virtual void StartProfiling() = 0; virtual void StopProfiling() = 0; virtual void ResetProfilingData() = 0; - virtual bool IsProfiling() const = 0; - virtual bool IsAnySaveOptionEnabled() const = 0; + [[nodiscard]] virtual bool IsProfiling() const = 0; + [[nodiscard]] virtual bool IsAnySaveOptionEnabled() const = 0; virtual void ChangeSavePath( - const AZ::IO::Path& newSavePath) = 0; //!< Caution! This function is not runtime safe. Instead use @ref SafeChangeSavePath + const AZ::IO::Path& newSavePath) = 0; //!< Caution! This function is not runtime safe. Instead, use @ref SafeChangeSavePath virtual void SafeChangeSavePath(const AZ::IO::Path& newSavePath) = 0; //!< Runtime safe path changing. Saves and stops current - //!< profiling and changes path afterwards. + //!< profiling and changes path afterward. // Get Fps Data - virtual float GetMinFps() const = 0; - virtual float GetMaxFps() const = 0; - virtual float GetAvgFps() const = 0; - virtual float GetCurrentFps() const = 0; + [[nodiscard]] virtual float GetMinFps() const = 0; + [[nodiscard]] virtual float GetMaxFps() const = 0; + [[nodiscard]] virtual float GetAvgFps() const = 0; + [[nodiscard]] virtual float GetCurrentFps() const = 0; // Memory usage - virtual size_t GetCpuMemoryUsed() const = 0; - virtual size_t GetGpuMemoryUsed() const = 0; + [[nodiscard]] virtual size_t GetCpuMemoryUsed() const = 0; + [[nodiscard]] virtual size_t GetGpuMemoryUsed() const = 0; // Logging virtual void SaveLogToFile() = 0; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 80a26ed0..f98c101c 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -300,7 +300,9 @@ namespace FPSProfiler } m_maxFps = AZStd::max(m_maxFps, m_currentFps); - m_avgFps = !m_fpsSamples.empty() ? (AZStd::accumulate(m_fpsSamples.begin(), m_fpsSamples.end(), 0.0f) / m_fpsSamples.size()) : 0.0f; + m_avgFps = !m_fpsSamples.empty() + ? (AZStd::accumulate(m_fpsSamples.begin(), m_fpsSamples.end(), 0.0f) / static_cast(m_fpsSamples.size())) + : 0.0f; } void FPSProfilerSystemComponent::CreateLogFile() @@ -340,7 +342,7 @@ namespace FPSProfiler std::time_t now_time_t = AZStd::chrono::system_clock::to_time_t(now); // Convert to local time structure - std::tm timeInfo; + std::tm timeInfo{}; localtime_r(&now_time_t, &timeInfo); // Format the timestamp as YYYYMMDD_HHMM diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 1c205e19..ac5dfd43 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -39,16 +39,16 @@ namespace FPSProfiler void StartProfiling() override; void StopProfiling() override; void ResetProfilingData() override; - bool IsProfiling() const override; - bool IsAnySaveOptionEnabled() const override; + [[nodiscard]] bool IsProfiling() const override; + [[nodiscard]] bool IsAnySaveOptionEnabled() const override; void ChangeSavePath(const AZ::IO::Path& newSavePath) override; void SafeChangeSavePath(const AZ::IO::Path& newSavePath) override; - float GetMinFps() const override; - float GetMaxFps() const override; - float GetAvgFps() const override; - float GetCurrentFps() const override; - size_t GetCpuMemoryUsed() const override; - size_t GetGpuMemoryUsed() const override; + [[nodiscard]] float GetMinFps() const override; + [[nodiscard]] float GetMaxFps() const override; + [[nodiscard]] float GetAvgFps() const override; + [[nodiscard]] float GetCurrentFps() const override; + [[nodiscard]] size_t GetCpuMemoryUsed() const override; + [[nodiscard]] size_t GetGpuMemoryUsed() const override; void SaveLogToFile() override; void SaveLogToFileWithNewPath(const AZStd::string& newSavePath, bool useSafeChangePath) override; void ShowFpsOnScreen(bool enable) override; From e706b4bab007fe020aaf794152120f451140c8a0 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 3 Mar 2025 11:53:23 +0100 Subject: [PATCH 054/175] valdiate path on activation Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index f98c101c..1fcffdda 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -56,7 +56,7 @@ namespace FPSProfiler void FPSProfilerSystemComponent::Activate() { - if (m_configuration.m_OutputFilename.empty()) + if (!IsPathValid(m_configuration.m_OutputFilename)) { m_configuration.m_OutputFilename = "@user@/fps_log.csv"; AZ_Warning("FPSProfiler", false, "Invalid output file path. Using default: %s", m_configuration.m_OutputFilename.c_str()); From 8b6bf19ad320915706f3716798ae9e62a64ba37f Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 3 Mar 2025 11:55:01 +0100 Subject: [PATCH 055/175] update reflect name Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index 19f78498..083f108d 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -12,7 +12,7 @@ namespace FPSProfiler ->Version(0) ->Field("m_OutputFilename", &FPSProfilerConfig::m_OutputFilename) ->Field("m_AutoSave", &FPSProfilerConfig::m_AutoSave) - ->Field("m_AutoSaveOccurrences", &FPSProfilerConfig::m_AutoSaveAtFrame) + ->Field("m_AutoSaveAtFrame", &FPSProfilerConfig::m_AutoSaveAtFrame) ->Field("m_SaveWithTimestamp", &FPSProfilerConfig::m_SaveWithTimestamp) ->Field("m_NearZeroPrecision", &FPSProfilerConfig::m_NearZeroPrecision) ->Field("m_SaveFPSData", &FPSProfilerConfig::m_SaveFpsData) From 20781213154f2909602fe4a630cb562c46d59b86 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 3 Mar 2025 12:01:43 +0100 Subject: [PATCH 056/175] fix file creation | timestamp Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 1fcffdda..0ee9e8d3 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -311,9 +311,29 @@ namespace FPSProfiler { m_configuration.m_OutputFilename = "@user@/fps_log.csv"; AZ_Warning("FPSProfiler", false, "Invalid output file path. Using default: %s", m_configuration.m_OutputFilename.c_str()); - return; } + // Apply Timestamp + if (m_configuration.m_SaveWithTimestamp) + { + // Get current system time + auto now = AZStd::chrono::system_clock::now(); + std::time_t now_time_t = AZStd::chrono::system_clock::to_time_t(now); + + // Convert to local time structure + std::tm timeInfo{}; + localtime_r(&now_time_t, &timeInfo); + + // Format the timestamp as YYYYMMDD_HHMM + char timestamp[16]; // Buffer for formatted time + strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M", &timeInfo); + + m_configuration.m_OutputFilename.ReplaceFilename( + (m_configuration.m_OutputFilename.Stem().String() + "_" + timestamp + m_configuration.m_OutputFilename.Extension().String()) + .data()); + } + + // Validate if file can be created AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); if (!fileIO->Exists(m_configuration.m_OutputFilename.c_str())) { @@ -335,25 +355,7 @@ namespace FPSProfiler } } - if (m_configuration.m_SaveWithTimestamp) - { - // Get current system time - auto now = AZStd::chrono::system_clock::now(); - std::time_t now_time_t = AZStd::chrono::system_clock::to_time_t(now); - - // Convert to local time structure - std::tm timeInfo{}; - localtime_r(&now_time_t, &timeInfo); - - // Format the timestamp as YYYYMMDD_HHMM - char timestamp[16]; // Buffer for formatted time - strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M", &timeInfo); - - m_configuration.m_OutputFilename.ReplaceFilename( - (m_configuration.m_OutputFilename.Stem().String() + "_" + timestamp + m_configuration.m_OutputFilename.Extension().String()) - .data()); - } - + // Write profiling headers to file AZ::IO::FileIOStream file(m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath); AZStd::string csvHeader = "Frame,FrameTime,CurrentFPS,MinFPS,MaxFPS,AvgFPS,CpuMemoryUsed,GpuMemoryUsed\n"; file.Write(csvHeader.size(), csvHeader.c_str()); From 2fcd35d9ec40812e5f38a8c8743de5b381418c39 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 3 Mar 2025 14:35:17 +0100 Subject: [PATCH 057/175] fix memory reserve Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 0ee9e8d3..32d82558 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -76,7 +76,7 @@ namespace FPSProfiler // Since log entries are cleared when occurrence update happens, it's good to reserve known size. if (m_configuration.m_AutoSave) { - m_fpsSamples.reserve(m_configuration.m_AutoSaveAtFrame * 2); + m_fpsSamples.reserve(m_configuration.m_AutoSaveAtFrame); m_logEntries.reserve(m_configuration.m_AutoSaveAtFrame * 2); } } From fce0b1f43057b53dc9d4f5c875e42306f3c7d4e8 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 3 Mar 2025 14:37:03 +0100 Subject: [PATCH 058/175] replace string with IO:PATH Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 76f3b1db..4fe39032 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -5,7 +5,6 @@ #include #include #include -#include namespace FPSProfiler { @@ -38,7 +37,7 @@ namespace FPSProfiler // Logging virtual void SaveLogToFile() = 0; - virtual void SaveLogToFileWithNewPath(const AZStd::string& newSavePath, bool useSafeChangePath) = 0; + virtual void SaveLogToFileWithNewPath(const AZ::IO::Path& newSavePath, bool useSafeChangePath) = 0; virtual void ShowFpsOnScreen(bool enable) = 0; }; @@ -62,13 +61,13 @@ namespace FPSProfiler AZ_RTTI(FPSProfilerNotifications, FPSProfilerNotificationsTypeId); virtual ~FPSProfilerNotifications() = default; - virtual void OnFileCreated(const AZStd::string& fileName) + virtual void OnFileCreated(const AZ::IO::Path& filePath) { } - virtual void OnFileUpdate(const AZStd::string& fileName) + virtual void OnFileUpdate(const AZ::IO::Path& filePath) { } - virtual void OnFileSaved(const AZStd::string& fileName) + virtual void OnFileSaved(const AZ::IO::Path& filePath) { } }; From d21c05fb0f39487702deab4f9f93dd51402d7786 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 3 Mar 2025 14:57:40 +0100 Subject: [PATCH 059/175] remove redundant code Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 28 ++----------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 32d82558..4213d47b 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -202,7 +202,7 @@ namespace FPSProfiler } m_configuration.m_OutputFilename = newSavePath; - AZ_Warning("FPS Profiler", false, "Path changed."); + AZ_Warning("FPS Profiler", !m_isProfiling, "Path changed during activated profiling."); } void FPSProfilerSystemComponent::SafeChangeSavePath(const AZ::IO::Path& newSavePath) @@ -316,16 +316,14 @@ namespace FPSProfiler // Apply Timestamp if (m_configuration.m_SaveWithTimestamp) { - // Get current system time auto now = AZStd::chrono::system_clock::now(); std::time_t now_time_t = AZStd::chrono::system_clock::to_time_t(now); - // Convert to local time structure std::tm timeInfo{}; localtime_r(&now_time_t, &timeInfo); // Format the timestamp as YYYYMMDD_HHMM - char timestamp[16]; // Buffer for formatted time + char timestamp[16]; strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M", &timeInfo); m_configuration.m_OutputFilename.ReplaceFilename( @@ -333,28 +331,6 @@ namespace FPSProfiler .data()); } - // Validate if file can be created - AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); - if (!fileIO->Exists(m_configuration.m_OutputFilename.c_str())) - { - AZ_Warning("FPSProfiler", false, "File does not exist, trying to create it..."); - - AZ::IO::HandleType fileHandle; - if (fileIO->Open( - m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath, fileHandle) == - AZ::IO::ResultCode::Success) - { - fileIO->Close(fileHandle); - AZ_Printf("FPSProfiler", "Log file successfully created: %s", m_configuration.m_OutputFilename.c_str()); - } - else - { - // Restore default path on fail - m_configuration.m_OutputFilename = "@user@/fps_log.csv"; - AZ_Warning("FPSProfiler", false, "Failed to create file. Using default path: %s", m_configuration.m_OutputFilename.c_str()); - } - } - // Write profiling headers to file AZ::IO::FileIOStream file(m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath); AZStd::string csvHeader = "Frame,FrameTime,CurrentFPS,MinFPS,MaxFPS,AvgFPS,CpuMemoryUsed,GpuMemoryUsed\n"; From 9a5543c743800cf0550e7abe2bdaed3fc7970080 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 3 Mar 2025 15:21:03 +0100 Subject: [PATCH 060/175] change string path to AZ:IO:PATH Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 +- .../Code/Source/Clients/FPSProfilerSystemComponent.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 4213d47b..59b1bf55 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -266,7 +266,7 @@ namespace FPSProfiler WriteDataToFile(); } - void FPSProfilerSystemComponent::SaveLogToFileWithNewPath(const AZStd::string& newSavePath, bool useSafeChangePath) + void FPSProfilerSystemComponent::SaveLogToFileWithNewPath(const AZ::IO::Path& newSavePath, bool useSafeChangePath) { if (useSafeChangePath) { diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index ac5dfd43..fe5a0226 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -50,7 +50,7 @@ namespace FPSProfiler [[nodiscard]] size_t GetCpuMemoryUsed() const override; [[nodiscard]] size_t GetGpuMemoryUsed() const override; void SaveLogToFile() override; - void SaveLogToFileWithNewPath(const AZStd::string& newSavePath, bool useSafeChangePath) override; + void SaveLogToFileWithNewPath(const AZ::IO::Path& newSavePath, bool useSafeChangePath) override; void ShowFpsOnScreen(bool enable) override; private: From 59ffb83c111f809b8168d40d17ee024a27d207c9 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 3 Mar 2025 15:31:17 +0100 Subject: [PATCH 061/175] use deque instead of vector Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 22 ++++++++++--------- .../Clients/FPSProfilerSystemComponent.h | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 59b1bf55..9e008b3b 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -72,11 +72,10 @@ namespace FPSProfiler CreateLogFile(); } - // Reserve at least twice as needed occurrences, since close and save operation may happen at the tick frame saves. - // Since log entries are cleared when occurrence update happens, it's good to reserve known size. + // Since log entries are cleared when occurrence update happens, it's good to reserve known size and some extra buffor for close + // operation. if (m_configuration.m_AutoSave) { - m_fpsSamples.reserve(m_configuration.m_AutoSaveAtFrame); m_logEntries.reserve(m_configuration.m_AutoSaveAtFrame * 2); } } @@ -288,21 +287,25 @@ namespace FPSProfiler void FPSProfilerSystemComponent::CalculateFpsData(const float& deltaTime) { m_currentFps = deltaTime > 0 ? (1.0f / deltaTime) : 0.0f; - m_fpsSamples.push_back(m_currentFps); - m_totalFrameTime += deltaTime; m_frameCount++; + // Latest fps hisotry for avg fps calculation + m_fpsSamples.push_back(m_currentFps); + if (m_fpsSamples.size() > m_configuration.m_AutoSaveAtFrame) + { + m_fpsSamples.pop_front(); + } + + m_avgFps = AZStd::accumulate(m_fpsSamples.begin(), m_fpsSamples.end(), 0.0f) / static_cast(m_fpsSamples.size()); + // Using m_NearZeroPrecision, since m_currentFPS cannot be equal to 0 if delta time is valid. if (m_currentFps >= m_configuration.m_NearZeroPrecision) { m_minFps = AZStd::min(m_minFps, m_currentFps); } - m_maxFps = AZStd::max(m_maxFps, m_currentFps); - m_avgFps = !m_fpsSamples.empty() - ? (AZStd::accumulate(m_fpsSamples.begin(), m_fpsSamples.end(), 0.0f) / static_cast(m_fpsSamples.size())) - : 0.0f; + m_maxFps = AZStd::max(m_maxFps, m_currentFps); } void FPSProfilerSystemComponent::CreateLogFile() @@ -360,7 +363,6 @@ namespace FPSProfiler file.Write(entry.size(), entry.c_str()); } file.Close(); - m_logEntries.clear(); m_fpsSamples.clear(); // Notify - File Update diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index fe5a0226..5d506774 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -65,7 +65,7 @@ namespace FPSProfiler float m_currentFps = 0.0f; // Actual FPS in current frame float m_totalFrameTime = 0.0f; // Time it took to enter frame int m_frameCount = 0; // Numeric value of actual frame - AZStd::vector m_fpsSamples; // Vector of collected current FPSs. Cleared once @ref m_configuration.m_AutoSave enabled. + AZStd::deque m_fpsSamples; // Vector of collected current FPSs. Cleared once @ref m_configuration.m_AutoSave enabled. AZStd::vector m_logEntries; // Vector of collected log entries. Cleared after @ref // m_configuration.m_AutoSaveOccurrences, when @ref m_configuration.m_AutoSave enabled. From 9180fd999d9120b03ae2b695451cc5422b460ded Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 3 Mar 2025 16:22:23 +0100 Subject: [PATCH 062/175] reflect missing variable Signed-off-by: Wojciech Czerski --- .../Source/Clients/FPSProfilerSystemComponent.cpp | 12 +++++++----- .../Code/Source/Clients/FPSProfilerSystemComponent.h | 2 +- .../Source/Tools/FPSProfilerEditorSystemComponent.h | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 9e008b3b..67b1f26f 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -13,8 +13,10 @@ namespace FPSProfiler { if (auto serializeContext = azrtti_cast(context)) { - serializeContext->Class()->Version(0)->Field( - "m_Configuration", &FPSProfilerSystemComponent::m_configuration); + serializeContext->Class() + ->Version(1) + ->Field("m_Configuration", &FPSProfilerSystemComponent::m_configuration) + ->Field("m_profileOnGameStart", &FPSProfilerSystemComponent::m_isProfiling); } } @@ -36,9 +38,9 @@ namespace FPSProfiler } } - FPSProfilerSystemComponent::FPSProfilerSystemComponent(FPSProfilerConfig m_configuration, bool m_profileOnGameStart) - : m_configuration(AZStd::move(m_configuration)) - , m_isProfiling(m_profileOnGameStart) + FPSProfilerSystemComponent::FPSProfilerSystemComponent(const FPSProfilerConfig& config, bool profileOnGameStart) + : m_configuration(AZStd::move(config)) + , m_isProfiling(profileOnGameStart) { if (FPSProfilerInterface::Get() == nullptr) { diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 5d506774..be0ccb78 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -23,7 +23,7 @@ namespace FPSProfiler static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); FPSProfilerSystemComponent(); - explicit FPSProfilerSystemComponent(FPSProfilerConfig m_configuration, bool m_profileOnGameStart); + explicit FPSProfilerSystemComponent(const FPSProfilerConfig& config, bool profileOnGameStart); ~FPSProfilerSystemComponent() override; protected: diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h index af7299cd..00508430 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h @@ -31,6 +31,6 @@ namespace FPSProfiler static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); FPSProfilerConfig m_configuration; - bool m_profileOnGameStart{}; + bool m_profileOnGameStart; }; } // namespace FPSProfiler From 543a6448db7ccc24b2e98d74047dd2d6ad453b1a Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 09:45:53 +0100 Subject: [PATCH 063/175] add info to gem.json Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/gem.json | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Gems/FPSProfiler/gem.json b/Gems/FPSProfiler/gem.json index c9755cc3..bca95a2a 100644 --- a/Gems/FPSProfiler/gem.json +++ b/Gems/FPSProfiler/gem.json @@ -4,10 +4,10 @@ "display_name": "FPSProfiler", "license": "License used i.e. Apache-2.0 or MIT", "license_url": "Link to the license web site i.e. https://opensource.org/licenses/Apache-2.0", - "origin": "The name of the originator or creator", - "origin_url": "The website for this Gem", + "origin": "RobotecAI", + "origin_url": "TBD", "type": "Code", - "summary": "A short description of this Gem", + "summary": "Allows profiling FPS, CPU, GPU data and collect statistics into csv file", "canonical_tags": [ "Gem" ], @@ -18,9 +18,11 @@ "" ], "icon_path": "preview.png", - "requirements": "Notice of any requirements for this Gem i.e. This requires X other gem", - "documentation_url": "Link to any documentation of your Gem", - "dependencies": [], + "requirements": "Requires Atom RHI dependency!", + "documentation_url": "Refer to the documentation available at repository.", + "dependencies": [ + "Atom_RHI" + ], "repo_uri": "", "compatible_engines": [], "engine_api_dependencies": [], From 73251a1c7883ed072ef1830b77c904dcc239818e Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 10:57:01 +0100 Subject: [PATCH 064/175] init readme Signed-off-by: Wojciech Czerski --- doc/FpsProfiler.png | Bin 0 -> 34298 bytes readme.md | 31 +++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 doc/FpsProfiler.png diff --git a/doc/FpsProfiler.png b/doc/FpsProfiler.png new file mode 100644 index 0000000000000000000000000000000000000000..215f1956a965472e8562961b34b2bc1d5931d089 GIT binary patch literal 34298 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 SOz>c!A8}zBp>hE||NjGIZvj#O literal 0 HcmV?d00001 diff --git a/readme.md b/readme.md index 387456ee..88982594 100644 --- a/readme.md +++ b/readme.md @@ -346,3 +346,34 @@ Refer to script canvas example below: ![alt text](doc/imguizmo.png) *Note* Only one gizmo can be rendered at the time! + +# FPSProfiler +This gem provides a tool to collect statistics in the game mode of the FPS, CPU and GPU into `csv` file. + +The Profiler has a EBus which can control profiling in runtime (start/stop/reset), save profiled data, change save path, access current frame memory data or fps (avg, min, max). +It is also provided with a set of notification functions, making it highly customizable for end user. + +## Setup +To start using the profiler, add a `FPSProfiler` to Level entity. +![FpsProfiler Editor](doc/FpsProfiler.png) + +| Variable Name | Description | +|---------------------------|--------------------------------------------------------------------------------------------| +| Csv Save Path | Path where collected data will be saved. | +| Auto Save | Enable auto save. | +| Auto Save At Frame | Auto saves collected data at selected frame occurrence. | +| Timestamp | Applies timestamp Year-Month-Day-Hour-Minutes to file name. Let's you save multiple files. | +| Near Zero Precision | | +| Save FPS Data | | +| Save CPU Data | | +| Save GPU Data | | +| Show FPS | | +| Profile On Game Start | | + +## Saved File Example +| Frame | FrameTime | CurrentFPS | MinFPS | MaxFPS | AvgFPS | CpuMemoryUsed | GpuMemoryUsed | +|-------|------------|------------|-----------|---------|---------|---------------|---------------| +| 1 | 5347.2783 | 0.00 | MAX_FLOAT | 0.00 | 0.00 | 1349.42 | 1752.12 | +| 2 | 0.4207 | 2.38 | 2.38 | 2.38 | 1.19 | 1375.50 | 2999.38 | +| 3 | 0.1934 | 5.17 | 2.38 | 5.17 | 2.52 | 1400.49 | 2963.44 | + From 21c6010873ef8a12b389e9ea794e36ab90459925 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 11:33:09 +0100 Subject: [PATCH 065/175] add context to readme Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 7 ++ .../FPSProfilerEditorSystemComponent.cpp | 16 +++++ readme.md | 69 +++++++++++++------ 3 files changed, 71 insertions(+), 21 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 4fe39032..191c55e3 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -70,6 +71,12 @@ namespace FPSProfiler virtual void OnFileSaved(const AZ::IO::Path& filePath) { } + virtual void OnProfileStart(const FPSProfilerConfig& config) + { + } + virtual void OnProfileStop(const FPSProfilerConfig& config) + { + } }; class FPSProfilerNotificationBusTraits : public AZ::EBusTraits diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 3ce35673..3c48242d 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -58,5 +58,21 @@ namespace FPSProfiler void FPSProfilerEditorSystemComponent::BuildGameEntity(AZ::Entity* entity) { entity->CreateComponent(m_configuration, m_profileOnGameStart); + auto profiler = FPSProfiler::FPSProfilerInterface::Get(); + if (profiler) + { + profiler->StartProfiling(); + profiler->GetCurrentFps(); + } + + float currentFps = 0.0f; + + // Retrieve FPS using the request bus + FPSProfilerRequestBus::BroadcastResult(currentFps, &FPSProfilerRequests::GetCurrentFps); + + // Broadcast FPS (Replace with a relevant event in your system) + AZ_Printf("FPSProfiler", "Current FPS: %.2f", currentFps); + + FPSProfilerRequestBus::Broadcast(&FPSProfilerRequests::StartProfiling); } } // namespace FPSProfiler diff --git a/readme.md b/readme.md index 88982594..97980663 100644 --- a/readme.md +++ b/readme.md @@ -353,27 +353,54 @@ This gem provides a tool to collect statistics in the game mode of the FPS, CPU The Profiler has a EBus which can control profiling in runtime (start/stop/reset), save profiled data, change save path, access current frame memory data or fps (avg, min, max). It is also provided with a set of notification functions, making it highly customizable for end user. -## Setup -To start using the profiler, add a `FPSProfiler` to Level entity. ![FpsProfiler Editor](doc/FpsProfiler.png) -| Variable Name | Description | -|---------------------------|--------------------------------------------------------------------------------------------| -| Csv Save Path | Path where collected data will be saved. | -| Auto Save | Enable auto save. | -| Auto Save At Frame | Auto saves collected data at selected frame occurrence. | -| Timestamp | Applies timestamp Year-Month-Day-Hour-Minutes to file name. Let's you save multiple files. | -| Near Zero Precision | | -| Save FPS Data | | -| Save CPU Data | | -| Save GPU Data | | -| Show FPS | | -| Profile On Game Start | | - -## Saved File Example -| Frame | FrameTime | CurrentFPS | MinFPS | MaxFPS | AvgFPS | CpuMemoryUsed | GpuMemoryUsed | -|-------|------------|------------|-----------|---------|---------|---------------|---------------| -| 1 | 5347.2783 | 0.00 | MAX_FLOAT | 0.00 | 0.00 | 1349.42 | 1752.12 | -| 2 | 0.4207 | 2.38 | 2.38 | 2.38 | 1.19 | 1375.50 | 2999.38 | -| 3 | 0.1934 | 5.17 | 2.38 | 5.17 | 2.52 | 1400.49 | 2963.44 | +| Variable Name | Description | +|----------------------------|----------------------------------------------------------------------------------------------------| +| **Csv Save Path** | Path where collected data will be saved. | +| **Auto Save** | Enable to auto save. Auto save is performed when target defined variable is reached. | +| **Auto Save At Frame** | Auto saves collected data at selected frame occurrence. | +| **Timestamp** | Applies timestamp Year-Month-Day-Hour-Minutes to file name. Let's you save multiple files at once. | +| **Near Zero Precision** | Floating point precision when comparing to 0. | +| **Save FPS Data** | Save collected FPS statistics. | +| **Save CPU Data** | Save collected CPU statistics. | +| **Save GPU Data** | Save collected GPU statistics. | +| **Show FPS** | Show the FPS value in the left-top corner. | +| **Profile On Game Start** | Start profiling data at once into csv file, after entering game mode. | + +## Setup +To start using the tool, add a `FPSProfiler` to the **Level** entity. + +### Profiling data using API Interface: +```c++ +// Get Interface and validate it +auto profiler = FPSProfiler::FPSProfilerInterface::Get(); +if (!profiler) +{ + return; +} + +profiler->StartProfiling(); +float currentFps = profiler->GetCurrentFps(); +``` + +### Profiling data using API Broadcast: +```c++ +// Start profiling +FPSProfilerRequestBus::Broadcast(&FPSProfilerRequests::StartProfiling); + +// Retrieve FPS using the request bus +float currentFps = 0.0f; +FPSProfilerRequestBus::BroadcastResult(currentFps, &FPSProfilerRequests::GetCurrentFps); +``` +## CSV File Example +| Frame | FrameTime | CurrentFPS | MinFPS | MaxFPS | AvgFPS | CpuMemoryUsed | GpuMemoryUsed | +|-------|--------------|--------------|-----------|-----------|-----------|------------------|-----------------| +| 1 | 5347.2783 | 0.00 | MAX_FLOAT | 0.00 | 0.00 | 1349.42 | 1752.12 | +| 2 | 0.4207 | 2.38 | 2.38 | 2.38 | 1.19 | 1375.50 | 2999.38 | +| 3 | 0.1934 | 5.17 | 2.38 | 5.17 | 2.52 | 1400.49 | 2963.44 | +| ... | ... | ... | ... | ... | ... | ... | ... | +| 100 | 0.0728 | 13.74 | 0.16 | 24.31 | 20.76 | 824.42 | 2553.17 | +| 101 | 0.1066 | 9.38 | 0.16 | 24.31 | 9.38 | 824.43 | 2595.25 | +| 102 | 0.0556 | 17.99 | 0.16 | 24.31 | 13.69 | 827.33 | 2595.25 | From b4bcbdb101a9217f67a9f1ed41b76953096ef3a6 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 11:45:33 +0100 Subject: [PATCH 066/175] add extra notification buses for stop/start/reset Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 3 +++ .../Clients/FPSProfilerSystemComponent.cpp | 9 +++++++++ .../Tools/FPSProfilerEditorSystemComponent.cpp | 16 ---------------- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 191c55e3..7dabbd0c 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -74,6 +74,9 @@ namespace FPSProfiler virtual void OnProfileStart(const FPSProfilerConfig& config) { } + virtual void OnProfileReset(const FPSProfilerConfig& config) + { + } virtual void OnProfileStop(const FPSProfilerConfig& config) { } diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 67b1f26f..09f888cd 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -153,6 +153,9 @@ namespace FPSProfiler { AZ::TickBus::Handler::BusConnect(); } + + // Notify - Profile Started + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileStart, m_configuration); AZ_Printf("FPS Profiler", "Profiling started."); } @@ -170,6 +173,9 @@ namespace FPSProfiler AZ::TickBus::Handler::BusDisconnect(); } SaveLogToFile(); + + // Notify - Profile Stopped + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileStop, m_configuration); AZ_Printf("FPS Profiler", "Profiling stopped."); } @@ -183,6 +189,9 @@ namespace FPSProfiler m_frameCount = 0; m_fpsSamples.clear(); m_logEntries.clear(); + + // Notify - Profile Reset + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileReset, m_configuration); } bool FPSProfilerSystemComponent::IsProfiling() const diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 3c48242d..3ce35673 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -58,21 +58,5 @@ namespace FPSProfiler void FPSProfilerEditorSystemComponent::BuildGameEntity(AZ::Entity* entity) { entity->CreateComponent(m_configuration, m_profileOnGameStart); - auto profiler = FPSProfiler::FPSProfilerInterface::Get(); - if (profiler) - { - profiler->StartProfiling(); - profiler->GetCurrentFps(); - } - - float currentFps = 0.0f; - - // Retrieve FPS using the request bus - FPSProfilerRequestBus::BroadcastResult(currentFps, &FPSProfilerRequests::GetCurrentFps); - - // Broadcast FPS (Replace with a relevant event in your system) - AZ_Printf("FPSProfiler", "Current FPS: %.2f", currentFps); - - FPSProfilerRequestBus::Broadcast(&FPSProfilerRequests::StartProfiling); } } // namespace FPSProfiler From 5f4e6126ec319349ef23f0ecce7d609cc4cb0acb Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 11:52:35 +0100 Subject: [PATCH 067/175] add print reset info Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 09f888cd..102673be 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -192,6 +192,7 @@ namespace FPSProfiler // Notify - Profile Reset FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileReset, m_configuration); + AZ_Printf("FPS Profiler", "Profiling reset."); } bool FPSProfilerSystemComponent::IsProfiling() const From b47751b1245018800d4ee0cf556e47771d9fa58b Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 11:55:37 +0100 Subject: [PATCH 068/175] restore version bump Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 102673be..bbc4303c 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -14,7 +14,7 @@ namespace FPSProfiler if (auto serializeContext = azrtti_cast(context)) { serializeContext->Class() - ->Version(1) + ->Version(0) ->Field("m_Configuration", &FPSProfilerSystemComponent::m_configuration) ->Field("m_profileOnGameStart", &FPSProfilerSystemComponent::m_isProfiling); } From 4032525b3186b21b2502e8beec382d4fa1f7d2d5 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 11:57:39 +0100 Subject: [PATCH 069/175] clear log entires fix Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index bbc4303c..24c6d3ec 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -375,7 +375,7 @@ namespace FPSProfiler file.Write(entry.size(), entry.c_str()); } file.Close(); - m_fpsSamples.clear(); + m_logEntries.clear(); // Notify - File Update FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileUpdate, m_configuration.m_OutputFilename.c_str()); From 83fe314610c2ebbd6c0ec4fbda1d6510f3490cee Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 11:59:45 +0100 Subject: [PATCH 070/175] fix comments for deque Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index be0ccb78..f36d2677 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -65,7 +65,7 @@ namespace FPSProfiler float m_currentFps = 0.0f; // Actual FPS in current frame float m_totalFrameTime = 0.0f; // Time it took to enter frame int m_frameCount = 0; // Numeric value of actual frame - AZStd::deque m_fpsSamples; // Vector of collected current FPSs. Cleared once @ref m_configuration.m_AutoSave enabled. + AZStd::deque m_fpsSamples; // Deque of collected current FPSs. Used for calculating @ref m_avgFps. AZStd::vector m_logEntries; // Vector of collected log entries. Cleared after @ref // m_configuration.m_AutoSaveOccurrences, when @ref m_configuration.m_AutoSave enabled. From 0c4440efe0aca11ba2c7f9d28010bfb768b98808 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 12:00:04 +0100 Subject: [PATCH 071/175] init editor variables Signed-off-by: Wojciech Czerski --- .../Code/Source/Tools/FPSProfilerEditorSystemComponent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h index 00508430..c27b4257 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h @@ -31,6 +31,6 @@ namespace FPSProfiler static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); FPSProfilerConfig m_configuration; - bool m_profileOnGameStart; + bool m_profileOnGameStart = false; }; } // namespace FPSProfiler From 106ebadf5ed7f3b1e279b2b1356ca8a26ba82591 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 12:01:59 +0100 Subject: [PATCH 072/175] update comment variable names Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index f36d2677..4ae1ff64 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -67,7 +67,7 @@ namespace FPSProfiler int m_frameCount = 0; // Numeric value of actual frame AZStd::deque m_fpsSamples; // Deque of collected current FPSs. Used for calculating @ref m_avgFps. AZStd::vector m_logEntries; // Vector of collected log entries. Cleared after @ref - // m_configuration.m_AutoSaveOccurrences, when @ref m_configuration.m_AutoSave enabled. + // m_configuration.m_AutoSaveAtFrame, when @ref m_configuration.m_AutoSave enabled. // File operations void CreateLogFile(); From e39c70d2ecb31ed72e181920136de1381d1fb4b5 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 13:05:40 +0100 Subject: [PATCH 073/175] remove string format | use char vector | fix buffer allocation Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 68 +++++++++++-------- .../Clients/FPSProfilerSystemComponent.h | 5 +- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 24c6d3ec..d7459a80 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -74,12 +74,9 @@ namespace FPSProfiler CreateLogFile(); } - // Since log entries are cleared when occurrence update happens, it's good to reserve known size and some extra buffor for close - // operation. - if (m_configuration.m_AutoSave) - { - m_logEntries.reserve(m_configuration.m_AutoSaveAtFrame * 2); - } + // Reserve log entries buffer size based on known auto save per frame + m_configuration.m_AutoSave ? m_logBuffer.reserve(160 * m_configuration.m_AutoSaveAtFrame * 2) + : m_logBuffer.reserve(MaxLogBufferSize); } void FPSProfilerSystemComponent::Deactivate() @@ -114,19 +111,37 @@ namespace FPSProfiler return; } - AZStd::string logEntry = AZStd::string::format( - "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n", - m_configuration.m_SaveFpsData ? m_frameCount : -1, - m_configuration.m_SaveFpsData ? deltaTime : -1.0f, - m_configuration.m_SaveFpsData ? m_currentFps : -1.0f, - m_configuration.m_SaveFpsData ? m_minFps : -1.0f, - m_configuration.m_SaveFpsData ? m_maxFps : -1.0f, - m_configuration.m_SaveFpsData ? m_avgFps : -1.0f, - m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed()) : -1.0f, - m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed()) : -1.0f); - m_logEntries.push_back(logEntry); - - // Save after every m_AutoSaveOccurrences frames to not overflow buffer, only when m_AutoSave enabled. + constexpr size_t LineSize = 128; + char logEntry[LineSize]; + int logEntryLength = 0; + + if (m_configuration.m_SaveFpsData) + { + logEntryLength = azsnprintf( + logEntry, + LineSize, + "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n", + m_frameCount, + deltaTime, + m_currentFps, + m_minFps, + m_maxFps, + m_avgFps, + m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed()) : -1.0f, + m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed()) : -1.0f); + } + else + { + logEntryLength = azsnprintf( + logEntry, + LineSize, + "-1,-1.0,-1.0,-1.0,-1.0,-1.0,%.2f,%.2f\n", + m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed()) : -1.0f, + m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed()) : -1.0f); + } + m_logBuffer.insert(m_logBuffer.end(), logEntry, logEntry + logEntryLength); + + // Auto Save if (m_configuration.m_AutoSave && m_frameCount % m_configuration.m_AutoSaveAtFrame == 0) { WriteDataToFile(); @@ -188,7 +203,7 @@ namespace FPSProfiler m_totalFrameTime = 0.0f; m_frameCount = 0; m_fpsSamples.clear(); - m_logEntries.clear(); + m_logBuffer.clear(); // Notify - Profile Reset FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileReset, m_configuration); @@ -358,7 +373,7 @@ namespace FPSProfiler void FPSProfilerSystemComponent::WriteDataToFile() { - if (m_logEntries.empty()) + if (m_logBuffer.empty()) { return; } @@ -368,14 +383,13 @@ namespace FPSProfiler return; } - AZ::IO::FileIOStream file(m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeAppend); - - for (const auto& entry : m_logEntries) + AZ::IO::HandleType file; + if (AZ::IO::FileIOBase::GetInstance()->Open(m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeAppend, file)) { - file.Write(entry.size(), entry.c_str()); + AZ::IO::FileIOBase::GetInstance()->Write(file, m_logBuffer.data(), m_logBuffer.size()); + AZ::IO::FileIOBase::GetInstance()->Close(file); } - file.Close(); - m_logEntries.clear(); + m_logBuffer.clear(); // Notify - File Update FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileUpdate, m_configuration.m_OutputFilename.c_str()); diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 4ae1ff64..b8dc10c7 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -66,8 +66,9 @@ namespace FPSProfiler float m_totalFrameTime = 0.0f; // Time it took to enter frame int m_frameCount = 0; // Numeric value of actual frame AZStd::deque m_fpsSamples; // Deque of collected current FPSs. Used for calculating @ref m_avgFps. - AZStd::vector m_logEntries; // Vector of collected log entries. Cleared after @ref - // m_configuration.m_AutoSaveAtFrame, when @ref m_configuration.m_AutoSave enabled. + AZStd::vector m_logBuffer; // Vector of collected log entries. Cleared after @ref + // m_configuration.m_AutoSaveAtFrame, when @ref m_configuration.m_AutoSave enabled. + static constexpr AZStd::size_t MaxLogBufferSize = 1024 * 128; // Max buffer size for @ref m_requiredLogBufferSize. // File operations void CreateLogFile(); From 0fecc4c42bc4b2e760cff8618050654fd7ad59c6 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 13:07:13 +0100 Subject: [PATCH 074/175] remove redundant comments Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index d7459a80..df8e0a3c 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -91,13 +91,10 @@ namespace FPSProfiler void FPSProfilerSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { - // Safety exit when profiling is disabled. if (!m_isProfiling) { return; } - - // Calculate data for Profiler Bus CalculateFpsData(deltaTime); if (m_configuration.m_ShowFps) @@ -105,7 +102,6 @@ namespace FPSProfiler ShowFps(); } - // If none save option enabled - exit if (!IsAnySaveOptionEnabled()) { return; From dc7a8a0472f7ec9e045f4aaea6c596eb8ee5787b Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 15:24:03 +0100 Subject: [PATCH 075/175] use Basic report flag | save 5.0ms Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index df8e0a3c..e89de219 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -273,7 +273,7 @@ namespace FPSProfiler if (AZ::RHI::Device* device = rhiSystem->GetDevice()) { AZ::RHI::MemoryStatistics memoryStats; - device->CompileMemoryStatistics(memoryStats, AZ::RHI::MemoryStatisticsReportFlags::Detail); + device->CompileMemoryStatistics(memoryStats, AZ::RHI::MemoryStatisticsReportFlags::Basic); // Return the GPU memory used in bytes return memoryStats.m_heaps.front().m_memoryUsage.m_totalResidentInBytes; From 4619a245f1818215887389fda33bf656ca19096b Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 15:24:16 +0100 Subject: [PATCH 076/175] remove comment Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index e89de219..3ff12658 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -275,7 +275,6 @@ namespace FPSProfiler AZ::RHI::MemoryStatistics memoryStats; device->CompileMemoryStatistics(memoryStats, AZ::RHI::MemoryStatisticsReportFlags::Basic); - // Return the GPU memory used in bytes return memoryStats.m_heaps.front().m_memoryUsage.m_totalResidentInBytes; } } From cf84709b17827df409afa83f44a226f07efac04c Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 15:31:03 +0100 Subject: [PATCH 077/175] remove redundant code Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 3ff12658..43ada5b4 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -91,6 +91,7 @@ namespace FPSProfiler void FPSProfilerSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { + AZ_PROFILE_FUNCTION(AzCore); if (!m_isProfiling) { return; @@ -259,10 +260,7 @@ namespace FPSProfiler size_t usedBytes = 0; size_t reservedBytes = 0; - // Get stats for the system allocator - AZ::AllocatorManager::Instance().GetAllocatorStats(usedBytes, reservedBytes, nullptr); - - // Return the used bytes (allocated memory) + AZ::AllocatorManager::Instance().GetAllocatorStats(usedBytes, reservedBytes); return usedBytes; } From aac47a904bc2efe2dfaa3eaf2c7b2d3c4f4442d5 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 15:34:14 +0100 Subject: [PATCH 078/175] clean up Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 3 +- .../Clients/FPSProfilerSystemComponent.h | 2 +- .../Code/3rdParty/ImGuizmo/ImGuizmo.cpp | 6196 +++++++++-------- .../Code/3rdParty/ImGuizmo/ImGuizmo.h | 278 +- 4 files changed, 3371 insertions(+), 3108 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 43ada5b4..30bd713b 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -76,7 +76,7 @@ namespace FPSProfiler // Reserve log entries buffer size based on known auto save per frame m_configuration.m_AutoSave ? m_logBuffer.reserve(160 * m_configuration.m_AutoSaveAtFrame * 2) - : m_logBuffer.reserve(MaxLogBufferSize); + : m_logBuffer.reserve(MAX_LOG_BUFFER_SIZE); } void FPSProfilerSystemComponent::Deactivate() @@ -91,7 +91,6 @@ namespace FPSProfiler void FPSProfilerSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { - AZ_PROFILE_FUNCTION(AzCore); if (!m_isProfiling) { return; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index b8dc10c7..ddec0a78 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -68,7 +68,7 @@ namespace FPSProfiler AZStd::deque m_fpsSamples; // Deque of collected current FPSs. Used for calculating @ref m_avgFps. AZStd::vector m_logBuffer; // Vector of collected log entries. Cleared after @ref // m_configuration.m_AutoSaveAtFrame, when @ref m_configuration.m_AutoSave enabled. - static constexpr AZStd::size_t MaxLogBufferSize = 1024 * 128; // Max buffer size for @ref m_requiredLogBufferSize. + static constexpr AZStd::size_t MAX_LOG_BUFFER_SIZE = 1024 * 128; // Max buffer size for @ref m_requiredLogBufferSize. // File operations void CreateLogFile(); diff --git a/Gems/ImGuizmo/Code/3rdParty/ImGuizmo/ImGuizmo.cpp b/Gems/ImGuizmo/Code/3rdParty/ImGuizmo/ImGuizmo.cpp index 204e3d8b..49afc2b3 100644 --- a/Gems/ImGuizmo/Code/3rdParty/ImGuizmo/ImGuizmo.cpp +++ b/Gems/ImGuizmo/Code/3rdParty/ImGuizmo/ImGuizmo.cpp @@ -27,9 +27,9 @@ #ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS #endif +#include "ImGuizmo.h" #include #include -#include "ImGuizmo.h" #if defined(_MSC_VER) || defined(__MINGW32__) #include @@ -45,1471 +45,1657 @@ namespace IMGUIZMO_NAMESPACE { - static const float ZPI = 3.14159265358979323846f; - static const float RAD2DEG = (180.f / ZPI); - static const float DEG2RAD = (ZPI / 180.f); - const float screenRotateSize = 0.06f; - // scale a bit so translate axis do not touch when in universal - const float rotationDisplayFactor = 1.2f; - - static OPERATION operator&(OPERATION lhs, OPERATION rhs) - { - return static_cast(static_cast(lhs) & static_cast(rhs)); - } - - static bool operator!=(OPERATION lhs, int rhs) - { - return static_cast(lhs) != rhs; - } - - static bool Intersects(OPERATION lhs, OPERATION rhs) - { - return (lhs & rhs) != 0; - } - - // True if lhs contains rhs - static bool Contains(OPERATION lhs, OPERATION rhs) - { - return (lhs & rhs) == rhs; - } - - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // utility and math - - void FPU_MatrixF_x_MatrixF(const float* a, const float* b, float* r) - { - r[0] = a[0] * b[0] + a[1] * b[4] + a[2] * b[8] + a[3] * b[12]; - r[1] = a[0] * b[1] + a[1] * b[5] + a[2] * b[9] + a[3] * b[13]; - r[2] = a[0] * b[2] + a[1] * b[6] + a[2] * b[10] + a[3] * b[14]; - r[3] = a[0] * b[3] + a[1] * b[7] + a[2] * b[11] + a[3] * b[15]; - - r[4] = a[4] * b[0] + a[5] * b[4] + a[6] * b[8] + a[7] * b[12]; - r[5] = a[4] * b[1] + a[5] * b[5] + a[6] * b[9] + a[7] * b[13]; - r[6] = a[4] * b[2] + a[5] * b[6] + a[6] * b[10] + a[7] * b[14]; - r[7] = a[4] * b[3] + a[5] * b[7] + a[6] * b[11] + a[7] * b[15]; - - r[8] = a[8] * b[0] + a[9] * b[4] + a[10] * b[8] + a[11] * b[12]; - r[9] = a[8] * b[1] + a[9] * b[5] + a[10] * b[9] + a[11] * b[13]; - r[10] = a[8] * b[2] + a[9] * b[6] + a[10] * b[10] + a[11] * b[14]; - r[11] = a[8] * b[3] + a[9] * b[7] + a[10] * b[11] + a[11] * b[15]; - - r[12] = a[12] * b[0] + a[13] * b[4] + a[14] * b[8] + a[15] * b[12]; - r[13] = a[12] * b[1] + a[13] * b[5] + a[14] * b[9] + a[15] * b[13]; - r[14] = a[12] * b[2] + a[13] * b[6] + a[14] * b[10] + a[15] * b[14]; - r[15] = a[12] * b[3] + a[13] * b[7] + a[14] * b[11] + a[15] * b[15]; - } - - void Frustum(float left, float right, float bottom, float top, float znear, float zfar, float* m16) - { - float temp, temp2, temp3, temp4; - temp = 2.0f * znear; - temp2 = right - left; - temp3 = top - bottom; - temp4 = zfar - znear; - m16[0] = temp / temp2; - m16[1] = 0.0; - m16[2] = 0.0; - m16[3] = 0.0; - m16[4] = 0.0; - m16[5] = temp / temp3; - m16[6] = 0.0; - m16[7] = 0.0; - m16[8] = (right + left) / temp2; - m16[9] = (top + bottom) / temp3; - m16[10] = (-zfar - znear) / temp4; - m16[11] = -1.0f; - m16[12] = 0.0; - m16[13] = 0.0; - m16[14] = (-temp * zfar) / temp4; - m16[15] = 0.0; - } - - void Perspective(float fovyInDegrees, float aspectRatio, float znear, float zfar, float* m16) - { - float ymax, xmax; - ymax = znear * tanf(fovyInDegrees * DEG2RAD); - xmax = ymax * aspectRatio; - Frustum(-xmax, xmax, -ymax, ymax, znear, zfar, m16); - } - - void Cross(const float* a, const float* b, float* r) - { - r[0] = a[1] * b[2] - a[2] * b[1]; - r[1] = a[2] * b[0] - a[0] * b[2]; - r[2] = a[0] * b[1] - a[1] * b[0]; - } - - float Dot(const float* a, const float* b) - { - return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; - } - - void Normalize(const float* a, float* r) - { - float il = 1.f / (sqrtf(Dot(a, a)) + FLT_EPSILON); - r[0] = a[0] * il; - r[1] = a[1] * il; - r[2] = a[2] * il; - } - - void LookAt(const float* eye, const float* at, const float* up, float* m16) - { - float X[3], Y[3], Z[3], tmp[3]; - - tmp[0] = eye[0] - at[0]; - tmp[1] = eye[1] - at[1]; - tmp[2] = eye[2] - at[2]; - Normalize(tmp, Z); - Normalize(up, Y); - Cross(Y, Z, tmp); - Normalize(tmp, X); - Cross(Z, X, tmp); - Normalize(tmp, Y); - - m16[0] = X[0]; - m16[1] = Y[0]; - m16[2] = Z[0]; - m16[3] = 0.0f; - m16[4] = X[1]; - m16[5] = Y[1]; - m16[6] = Z[1]; - m16[7] = 0.0f; - m16[8] = X[2]; - m16[9] = Y[2]; - m16[10] = Z[2]; - m16[11] = 0.0f; - m16[12] = -Dot(X, eye); - m16[13] = -Dot(Y, eye); - m16[14] = -Dot(Z, eye); - m16[15] = 1.0f; - } - - template T Clamp(T x, T y, T z) { return ((x < y) ? y : ((x > z) ? z : x)); } - template T max(T x, T y) { return (x > y) ? x : y; } - template T min(T x, T y) { return (x < y) ? x : y; } - template bool IsWithin(T x, T y, T z) { return (x >= y) && (x <= z); } - - struct matrix_t; - struct vec_t - { - public: - float x, y, z, w; - - void Lerp(const vec_t& v, float t) - { - x += (v.x - x) * t; - y += (v.y - y) * t; - z += (v.z - z) * t; - w += (v.w - w) * t; - } - - void Set(float v) { x = y = z = w = v; } - void Set(float _x, float _y, float _z = 0.f, float _w = 0.f) { x = _x; y = _y; z = _z; w = _w; } - - vec_t& operator -= (const vec_t& v) { x -= v.x; y -= v.y; z -= v.z; w -= v.w; return *this; } - vec_t& operator += (const vec_t& v) { x += v.x; y += v.y; z += v.z; w += v.w; return *this; } - vec_t& operator *= (const vec_t& v) { x *= v.x; y *= v.y; z *= v.z; w *= v.w; return *this; } - vec_t& operator *= (float v) { x *= v; y *= v; z *= v; w *= v; return *this; } - - vec_t operator * (float f) const; - vec_t operator - () const; - vec_t operator - (const vec_t& v) const; - vec_t operator + (const vec_t& v) const; - vec_t operator * (const vec_t& v) const; - - const vec_t& operator + () const { return (*this); } - float Length() const { return sqrtf(x * x + y * y + z * z); }; - float LengthSq() const { return (x * x + y * y + z * z); }; - vec_t Normalize() { (*this) *= (1.f / ( Length() > FLT_EPSILON ? Length() : FLT_EPSILON ) ); return (*this); } - vec_t Normalize(const vec_t& v) { this->Set(v.x, v.y, v.z, v.w); this->Normalize(); return (*this); } - vec_t Abs() const; - - void Cross(const vec_t& v) - { - vec_t res; - res.x = y * v.z - z * v.y; - res.y = z * v.x - x * v.z; - res.z = x * v.y - y * v.x; - - x = res.x; - y = res.y; - z = res.z; - w = 0.f; - } - - void Cross(const vec_t& v1, const vec_t& v2) - { - x = v1.y * v2.z - v1.z * v2.y; - y = v1.z * v2.x - v1.x * v2.z; - z = v1.x * v2.y - v1.y * v2.x; - w = 0.f; - } - - float Dot(const vec_t& v) const - { - return (x * v.x) + (y * v.y) + (z * v.z) + (w * v.w); - } - - float Dot3(const vec_t& v) const - { - return (x * v.x) + (y * v.y) + (z * v.z); - } - - void Transform(const matrix_t& matrix); - void Transform(const vec_t& s, const matrix_t& matrix); - - void TransformVector(const matrix_t& matrix); - void TransformPoint(const matrix_t& matrix); - void TransformVector(const vec_t& v, const matrix_t& matrix) { (*this) = v; this->TransformVector(matrix); } - void TransformPoint(const vec_t& v, const matrix_t& matrix) { (*this) = v; this->TransformPoint(matrix); } - - float& operator [] (size_t index) { return ((float*)&x)[index]; } - const float& operator [] (size_t index) const { return ((float*)&x)[index]; } - bool operator!=(const vec_t& other) const { return memcmp(this, &other, sizeof(vec_t)) != 0; } - }; - - vec_t makeVect(float _x, float _y, float _z = 0.f, float _w = 0.f) { vec_t res; res.x = _x; res.y = _y; res.z = _z; res.w = _w; return res; } - vec_t makeVect(ImVec2 v) { vec_t res; res.x = v.x; res.y = v.y; res.z = 0.f; res.w = 0.f; return res; } - vec_t vec_t::operator * (float f) const { return makeVect(x * f, y * f, z * f, w * f); } - vec_t vec_t::operator - () const { return makeVect(-x, -y, -z, -w); } - vec_t vec_t::operator - (const vec_t& v) const { return makeVect(x - v.x, y - v.y, z - v.z, w - v.w); } - vec_t vec_t::operator + (const vec_t& v) const { return makeVect(x + v.x, y + v.y, z + v.z, w + v.w); } - vec_t vec_t::operator * (const vec_t& v) const { return makeVect(x * v.x, y * v.y, z * v.z, w * v.w); } - vec_t vec_t::Abs() const { return makeVect(fabsf(x), fabsf(y), fabsf(z)); } - - vec_t Normalized(const vec_t& v) { vec_t res; res = v; res.Normalize(); return res; } - vec_t Cross(const vec_t& v1, const vec_t& v2) - { - vec_t res; - res.x = v1.y * v2.z - v1.z * v2.y; - res.y = v1.z * v2.x - v1.x * v2.z; - res.z = v1.x * v2.y - v1.y * v2.x; - res.w = 0.f; - return res; - } - - float Dot(const vec_t& v1, const vec_t& v2) - { - return (v1.x * v2.x) + (v1.y * v2.y) + (v1.z * v2.z); - } - - vec_t BuildPlan(const vec_t& p_point1, const vec_t& p_normal) - { - vec_t normal, res; - normal.Normalize(p_normal); - res.w = normal.Dot(p_point1); - res.x = normal.x; - res.y = normal.y; - res.z = normal.z; - return res; - } - - struct matrix_t - { - public: - - union - { - float m[4][4]; - float m16[16]; - struct - { - vec_t right, up, dir, position; - } v; - vec_t component[4]; - }; - - operator float* () { return m16; } - operator const float* () const { return m16; } - void Translation(float _x, float _y, float _z) { this->Translation(makeVect(_x, _y, _z)); } - - void Translation(const vec_t& vt) - { - v.right.Set(1.f, 0.f, 0.f, 0.f); - v.up.Set(0.f, 1.f, 0.f, 0.f); - v.dir.Set(0.f, 0.f, 1.f, 0.f); - v.position.Set(vt.x, vt.y, vt.z, 1.f); - } - - void Scale(float _x, float _y, float _z) - { - v.right.Set(_x, 0.f, 0.f, 0.f); - v.up.Set(0.f, _y, 0.f, 0.f); - v.dir.Set(0.f, 0.f, _z, 0.f); - v.position.Set(0.f, 0.f, 0.f, 1.f); - } - void Scale(const vec_t& s) { Scale(s.x, s.y, s.z); } - - matrix_t& operator *= (const matrix_t& mat) - { - matrix_t tmpMat; - tmpMat = *this; - tmpMat.Multiply(mat); - *this = tmpMat; - return *this; - } - matrix_t operator * (const matrix_t& mat) const - { - matrix_t matT; - matT.Multiply(*this, mat); - return matT; - } - - void Multiply(const matrix_t& matrix) - { - matrix_t tmp; - tmp = *this; - - FPU_MatrixF_x_MatrixF((float*)&tmp, (float*)&matrix, (float*)this); - } - - void Multiply(const matrix_t& m1, const matrix_t& m2) - { - FPU_MatrixF_x_MatrixF((float*)&m1, (float*)&m2, (float*)this); - } - - float GetDeterminant() const - { - return m[0][0] * m[1][1] * m[2][2] + m[0][1] * m[1][2] * m[2][0] + m[0][2] * m[1][0] * m[2][1] - - m[0][2] * m[1][1] * m[2][0] - m[0][1] * m[1][0] * m[2][2] - m[0][0] * m[1][2] * m[2][1]; - } - - float Inverse(const matrix_t& srcMatrix, bool affine = false); - void SetToIdentity() - { - v.right.Set(1.f, 0.f, 0.f, 0.f); - v.up.Set(0.f, 1.f, 0.f, 0.f); - v.dir.Set(0.f, 0.f, 1.f, 0.f); - v.position.Set(0.f, 0.f, 0.f, 1.f); - } - void Transpose() - { - matrix_t tmpm; - for (int l = 0; l < 4; l++) - { - for (int c = 0; c < 4; c++) - { - tmpm.m[l][c] = m[c][l]; - } - } - (*this) = tmpm; - } - - void RotationAxis(const vec_t& axis, float angle); - - void OrthoNormalize() - { - v.right.Normalize(); - v.up.Normalize(); - v.dir.Normalize(); - } - }; - - void vec_t::Transform(const matrix_t& matrix) - { - vec_t out; - - out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0] + w * matrix.m[3][0]; - out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1] + w * matrix.m[3][1]; - out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2] + w * matrix.m[3][2]; - out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3] + w * matrix.m[3][3]; - - x = out.x; - y = out.y; - z = out.z; - w = out.w; - } - - void vec_t::Transform(const vec_t& s, const matrix_t& matrix) - { - *this = s; - Transform(matrix); - } - - void vec_t::TransformPoint(const matrix_t& matrix) - { - vec_t out; - - out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0] + matrix.m[3][0]; - out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1] + matrix.m[3][1]; - out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2] + matrix.m[3][2]; - out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3] + matrix.m[3][3]; - - x = out.x; - y = out.y; - z = out.z; - w = out.w; - } - - void vec_t::TransformVector(const matrix_t& matrix) - { - vec_t out; - - out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0]; - out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1]; - out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2]; - out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3]; - - x = out.x; - y = out.y; - z = out.z; - w = out.w; - } - - float matrix_t::Inverse(const matrix_t& srcMatrix, bool affine) - { - float det = 0; - - if (affine) - { - det = GetDeterminant(); - float s = 1 / det; - m[0][0] = (srcMatrix.m[1][1] * srcMatrix.m[2][2] - srcMatrix.m[1][2] * srcMatrix.m[2][1]) * s; - m[0][1] = (srcMatrix.m[2][1] * srcMatrix.m[0][2] - srcMatrix.m[2][2] * srcMatrix.m[0][1]) * s; - m[0][2] = (srcMatrix.m[0][1] * srcMatrix.m[1][2] - srcMatrix.m[0][2] * srcMatrix.m[1][1]) * s; - m[1][0] = (srcMatrix.m[1][2] * srcMatrix.m[2][0] - srcMatrix.m[1][0] * srcMatrix.m[2][2]) * s; - m[1][1] = (srcMatrix.m[2][2] * srcMatrix.m[0][0] - srcMatrix.m[2][0] * srcMatrix.m[0][2]) * s; - m[1][2] = (srcMatrix.m[0][2] * srcMatrix.m[1][0] - srcMatrix.m[0][0] * srcMatrix.m[1][2]) * s; - m[2][0] = (srcMatrix.m[1][0] * srcMatrix.m[2][1] - srcMatrix.m[1][1] * srcMatrix.m[2][0]) * s; - m[2][1] = (srcMatrix.m[2][0] * srcMatrix.m[0][1] - srcMatrix.m[2][1] * srcMatrix.m[0][0]) * s; - m[2][2] = (srcMatrix.m[0][0] * srcMatrix.m[1][1] - srcMatrix.m[0][1] * srcMatrix.m[1][0]) * s; - m[3][0] = -(m[0][0] * srcMatrix.m[3][0] + m[1][0] * srcMatrix.m[3][1] + m[2][0] * srcMatrix.m[3][2]); - m[3][1] = -(m[0][1] * srcMatrix.m[3][0] + m[1][1] * srcMatrix.m[3][1] + m[2][1] * srcMatrix.m[3][2]); - m[3][2] = -(m[0][2] * srcMatrix.m[3][0] + m[1][2] * srcMatrix.m[3][1] + m[2][2] * srcMatrix.m[3][2]); - } - else - { - // transpose matrix - float src[16]; - for (int i = 0; i < 4; ++i) - { - src[i] = srcMatrix.m16[i * 4]; - src[i + 4] = srcMatrix.m16[i * 4 + 1]; - src[i + 8] = srcMatrix.m16[i * 4 + 2]; - src[i + 12] = srcMatrix.m16[i * 4 + 3]; - } - - // calculate pairs for first 8 elements (cofactors) - float tmp[12]; // temp array for pairs - tmp[0] = src[10] * src[15]; - tmp[1] = src[11] * src[14]; - tmp[2] = src[9] * src[15]; - tmp[3] = src[11] * src[13]; - tmp[4] = src[9] * src[14]; - tmp[5] = src[10] * src[13]; - tmp[6] = src[8] * src[15]; - tmp[7] = src[11] * src[12]; - tmp[8] = src[8] * src[14]; - tmp[9] = src[10] * src[12]; - tmp[10] = src[8] * src[13]; - tmp[11] = src[9] * src[12]; - - // calculate first 8 elements (cofactors) - m16[0] = (tmp[0] * src[5] + tmp[3] * src[6] + tmp[4] * src[7]) - (tmp[1] * src[5] + tmp[2] * src[6] + tmp[5] * src[7]); - m16[1] = (tmp[1] * src[4] + tmp[6] * src[6] + tmp[9] * src[7]) - (tmp[0] * src[4] + tmp[7] * src[6] + tmp[8] * src[7]); - m16[2] = (tmp[2] * src[4] + tmp[7] * src[5] + tmp[10] * src[7]) - (tmp[3] * src[4] + tmp[6] * src[5] + tmp[11] * src[7]); - m16[3] = (tmp[5] * src[4] + tmp[8] * src[5] + tmp[11] * src[6]) - (tmp[4] * src[4] + tmp[9] * src[5] + tmp[10] * src[6]); - m16[4] = (tmp[1] * src[1] + tmp[2] * src[2] + tmp[5] * src[3]) - (tmp[0] * src[1] + tmp[3] * src[2] + tmp[4] * src[3]); - m16[5] = (tmp[0] * src[0] + tmp[7] * src[2] + tmp[8] * src[3]) - (tmp[1] * src[0] + tmp[6] * src[2] + tmp[9] * src[3]); - m16[6] = (tmp[3] * src[0] + tmp[6] * src[1] + tmp[11] * src[3]) - (tmp[2] * src[0] + tmp[7] * src[1] + tmp[10] * src[3]); - m16[7] = (tmp[4] * src[0] + tmp[9] * src[1] + tmp[10] * src[2]) - (tmp[5] * src[0] + tmp[8] * src[1] + tmp[11] * src[2]); - - // calculate pairs for second 8 elements (cofactors) - tmp[0] = src[2] * src[7]; - tmp[1] = src[3] * src[6]; - tmp[2] = src[1] * src[7]; - tmp[3] = src[3] * src[5]; - tmp[4] = src[1] * src[6]; - tmp[5] = src[2] * src[5]; - tmp[6] = src[0] * src[7]; - tmp[7] = src[3] * src[4]; - tmp[8] = src[0] * src[6]; - tmp[9] = src[2] * src[4]; - tmp[10] = src[0] * src[5]; - tmp[11] = src[1] * src[4]; - - // calculate second 8 elements (cofactors) - m16[8] = (tmp[0] * src[13] + tmp[3] * src[14] + tmp[4] * src[15]) - (tmp[1] * src[13] + tmp[2] * src[14] + tmp[5] * src[15]); - m16[9] = (tmp[1] * src[12] + tmp[6] * src[14] + tmp[9] * src[15]) - (tmp[0] * src[12] + tmp[7] * src[14] + tmp[8] * src[15]); - m16[10] = (tmp[2] * src[12] + tmp[7] * src[13] + tmp[10] * src[15]) - (tmp[3] * src[12] + tmp[6] * src[13] + tmp[11] * src[15]); - m16[11] = (tmp[5] * src[12] + tmp[8] * src[13] + tmp[11] * src[14]) - (tmp[4] * src[12] + tmp[9] * src[13] + tmp[10] * src[14]); - m16[12] = (tmp[2] * src[10] + tmp[5] * src[11] + tmp[1] * src[9]) - (tmp[4] * src[11] + tmp[0] * src[9] + tmp[3] * src[10]); - m16[13] = (tmp[8] * src[11] + tmp[0] * src[8] + tmp[7] * src[10]) - (tmp[6] * src[10] + tmp[9] * src[11] + tmp[1] * src[8]); - m16[14] = (tmp[6] * src[9] + tmp[11] * src[11] + tmp[3] * src[8]) - (tmp[10] * src[11] + tmp[2] * src[8] + tmp[7] * src[9]); - m16[15] = (tmp[10] * src[10] + tmp[4] * src[8] + tmp[9] * src[9]) - (tmp[8] * src[9] + tmp[11] * src[10] + tmp[5] * src[8]); - - // calculate determinant - det = src[0] * m16[0] + src[1] * m16[1] + src[2] * m16[2] + src[3] * m16[3]; - - // calculate matrix inverse - float invdet = 1 / det; - for (int j = 0; j < 16; ++j) - { - m16[j] *= invdet; - } - } - - return det; - } - - void matrix_t::RotationAxis(const vec_t& axis, float angle) - { - float length2 = axis.LengthSq(); - if (length2 < FLT_EPSILON) - { - SetToIdentity(); - return; - } - - vec_t n = axis * (1.f / sqrtf(length2)); - float s = sinf(angle); - float c = cosf(angle); - float k = 1.f - c; - - float xx = n.x * n.x * k + c; - float yy = n.y * n.y * k + c; - float zz = n.z * n.z * k + c; - float xy = n.x * n.y * k; - float yz = n.y * n.z * k; - float zx = n.z * n.x * k; - float xs = n.x * s; - float ys = n.y * s; - float zs = n.z * s; - - m[0][0] = xx; - m[0][1] = xy + zs; - m[0][2] = zx - ys; - m[0][3] = 0.f; - m[1][0] = xy - zs; - m[1][1] = yy; - m[1][2] = yz + xs; - m[1][3] = 0.f; - m[2][0] = zx + ys; - m[2][1] = yz - xs; - m[2][2] = zz; - m[2][3] = 0.f; - m[3][0] = 0.f; - m[3][1] = 0.f; - m[3][2] = 0.f; - m[3][3] = 1.f; - } - - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // - - enum MOVETYPE - { - MT_NONE, - MT_MOVE_X, - MT_MOVE_Y, - MT_MOVE_Z, - MT_MOVE_YZ, - MT_MOVE_ZX, - MT_MOVE_XY, - MT_MOVE_SCREEN, - MT_ROTATE_X, - MT_ROTATE_Y, - MT_ROTATE_Z, - MT_ROTATE_SCREEN, - MT_SCALE_X, - MT_SCALE_Y, - MT_SCALE_Z, - MT_SCALE_XYZ - }; - - static bool IsTranslateType(int type) - { - return type >= MT_MOVE_X && type <= MT_MOVE_SCREEN; - } - - static bool IsRotateType(int type) - { - return type >= MT_ROTATE_X && type <= MT_ROTATE_SCREEN; - } - - static bool IsScaleType(int type) - { - return type >= MT_SCALE_X && type <= MT_SCALE_XYZ; - } - - // Matches MT_MOVE_AB order - static const OPERATION TRANSLATE_PLANS[3] = { TRANSLATE_Y | TRANSLATE_Z, TRANSLATE_X | TRANSLATE_Z, TRANSLATE_X | TRANSLATE_Y }; - - Style::Style() - { - // default values - TranslationLineThickness = 3.0f; - TranslationLineArrowSize = 6.0f; - RotationLineThickness = 2.0f; - RotationOuterLineThickness = 3.0f; - ScaleLineThickness = 3.0f; - ScaleLineCircleSize = 6.0f; - HatchedAxisLineThickness = 6.0f; - CenterCircleSize = 6.0f; - - // initialize default colors - Colors[DIRECTION_X] = ImVec4(0.666f, 0.000f, 0.000f, 1.000f); - Colors[DIRECTION_Y] = ImVec4(0.000f, 0.666f, 0.000f, 1.000f); - Colors[DIRECTION_Z] = ImVec4(0.000f, 0.000f, 0.666f, 1.000f); - Colors[PLANE_X] = ImVec4(0.666f, 0.000f, 0.000f, 0.380f); - Colors[PLANE_Y] = ImVec4(0.000f, 0.666f, 0.000f, 0.380f); - Colors[PLANE_Z] = ImVec4(0.000f, 0.000f, 0.666f, 0.380f); - Colors[SELECTION] = ImVec4(1.000f, 0.500f, 0.062f, 0.541f); - Colors[INACTIVE] = ImVec4(0.600f, 0.600f, 0.600f, 0.600f); - Colors[TRANSLATION_LINE] = ImVec4(0.666f, 0.666f, 0.666f, 0.666f); - Colors[SCALE_LINE] = ImVec4(0.250f, 0.250f, 0.250f, 1.000f); - Colors[ROTATION_USING_BORDER] = ImVec4(1.000f, 0.500f, 0.062f, 1.000f); - Colors[ROTATION_USING_FILL] = ImVec4(1.000f, 0.500f, 0.062f, 0.500f); - Colors[HATCHED_AXIS_LINES] = ImVec4(0.000f, 0.000f, 0.000f, 0.500f); - Colors[TEXT] = ImVec4(1.000f, 1.000f, 1.000f, 1.000f); - Colors[TEXT_SHADOW] = ImVec4(0.000f, 0.000f, 0.000f, 1.000f); - } - - struct Context - { - Context() : mbUsing(false), mbUsingViewManipulate(false), mbEnable(true), mIsViewManipulatorHovered(false), mbUsingBounds(false) - { - } - - ImDrawList* mDrawList; - Style mStyle; - - MODE mMode; - matrix_t mViewMat; - matrix_t mProjectionMat; - matrix_t mModel; - matrix_t mModelLocal; // orthonormalized model - matrix_t mModelInverse; - matrix_t mModelSource; - matrix_t mModelSourceInverse; - matrix_t mMVP; - matrix_t mMVPLocal; // MVP with full model matrix whereas mMVP's model matrix might only be translation in case of World space edition - matrix_t mViewProjection; - - vec_t mModelScaleOrigin; - vec_t mCameraEye; - vec_t mCameraRight; - vec_t mCameraDir; - vec_t mCameraUp; - vec_t mRayOrigin; - vec_t mRayVector; - - float mRadiusSquareCenter; - ImVec2 mScreenSquareCenter; - ImVec2 mScreenSquareMin; - ImVec2 mScreenSquareMax; - - float mScreenFactor; - vec_t mRelativeOrigin; - - bool mbUsing; - bool mbUsingViewManipulate; - bool mbEnable; - bool mbMouseOver; - bool mReversed; // reversed projection matrix - bool mIsViewManipulatorHovered; - - // translation - vec_t mTranslationPlan; - vec_t mTranslationPlanOrigin; - vec_t mMatrixOrigin; - vec_t mTranslationLastDelta; - - // rotation - vec_t mRotationVectorSource; - float mRotationAngle; - float mRotationAngleOrigin; - //vec_t mWorldToLocalAxis; - - // scale - vec_t mScale; - vec_t mScaleValueOrigin; - vec_t mScaleLast; - float mSaveMousePosx; - - // save axis factor when using gizmo - bool mBelowAxisLimit[3]; - int mAxisMask = 0; - bool mBelowPlaneLimit[3]; - float mAxisFactor[3]; - - float mAxisLimit=0.0025f; - float mPlaneLimit=0.02f; - - // bounds stretching - vec_t mBoundsPivot; - vec_t mBoundsAnchor; - vec_t mBoundsPlan; - vec_t mBoundsLocalPivot; - int mBoundsBestAxis; - int mBoundsAxis[2]; - bool mbUsingBounds; - matrix_t mBoundsMatrix; - - // - int mCurrentOperation; - - float mX = 0.f; - float mY = 0.f; - float mWidth = 0.f; - float mHeight = 0.f; - float mXMax = 0.f; - float mYMax = 0.f; - float mDisplayRatio = 1.f; - - bool mIsOrthographic = false; - // check to not have multiple gizmo highlighted at the same time - bool mbOverGizmoHotspot = false; - - ImGuiWindow* mAlternativeWindow = nullptr; - ImVector mIDStack; - ImGuiID mEditingID = -1; - OPERATION mOperation = OPERATION(-1); - - bool mAllowAxisFlip = true; - float mGizmoSizeClipSpace = 0.1f; - - inline ImGuiID GetCurrentID() - { - if (mIDStack.empty()) - { - mIDStack.push_back(-1); - } - return mIDStack.back(); - } - }; - - static Context gContext; - - static const vec_t directionUnary[3] = { makeVect(1.f, 0.f, 0.f), makeVect(0.f, 1.f, 0.f), makeVect(0.f, 0.f, 1.f) }; - static const char* translationInfoMask[] = { "X : %5.3f", "Y : %5.3f", "Z : %5.3f", - "Y : %5.3f Z : %5.3f", "X : %5.3f Z : %5.3f", "X : %5.3f Y : %5.3f", - "X : %5.3f Y : %5.3f Z : %5.3f" }; - static const char* scaleInfoMask[] = { "X : %5.2f", "Y : %5.2f", "Z : %5.2f", "XYZ : %5.2f" }; - static const char* rotationInfoMask[] = { "X : %5.2f deg %5.2f rad", "Y : %5.2f deg %5.2f rad", "Z : %5.2f deg %5.2f rad", "Screen : %5.2f deg %5.2f rad" }; - static const int translationInfoIndex[] = { 0,0,0, 1,0,0, 2,0,0, 1,2,0, 0,2,0, 0,1,0, 0,1,2 }; - static const float quadMin = 0.5f; - static const float quadMax = 0.8f; - static const float quadUV[8] = { quadMin, quadMin, quadMin, quadMax, quadMax, quadMax, quadMax, quadMin }; - static const int halfCircleSegmentCount = 64; - static const float snapTension = 0.5f; - - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // - static int GetMoveType(OPERATION op, vec_t* gizmoHitProportion); - static int GetRotateType(OPERATION op); - static int GetScaleType(OPERATION op); - - Style& GetStyle() - { - return gContext.mStyle; - } - - static ImU32 GetColorU32(int idx) - { - IM_ASSERT(idx < COLOR::COUNT); - return ImGui::ColorConvertFloat4ToU32(gContext.mStyle.Colors[idx]); - } - - static ImVec2 worldToPos(const vec_t& worldPos, const matrix_t& mat, ImVec2 position = ImVec2(gContext.mX, gContext.mY), ImVec2 size = ImVec2(gContext.mWidth, gContext.mHeight)) - { - vec_t trans; - trans.TransformPoint(worldPos, mat); - trans *= 0.5f / trans.w; - trans += makeVect(0.5f, 0.5f); - trans.y = 1.f - trans.y; - trans.x *= size.x; - trans.y *= size.y; - trans.x += position.x; - trans.y += position.y; - return ImVec2(trans.x, trans.y); - } - - static void ComputeCameraRay(vec_t& rayOrigin, vec_t& rayDir, ImVec2 position = ImVec2(gContext.mX, gContext.mY), ImVec2 size = ImVec2(gContext.mWidth, gContext.mHeight)) - { - ImGuiIO& io = ImGui::GetIO(); - - matrix_t mViewProjInverse; - mViewProjInverse.Inverse(gContext.mViewMat * gContext.mProjectionMat); - - const float mox = ((io.MousePos.x - position.x) / size.x) * 2.f - 1.f; - const float moy = (1.f - ((io.MousePos.y - position.y) / size.y)) * 2.f - 1.f; - - const float zNear = gContext.mReversed ? (1.f - FLT_EPSILON) : 0.f; - const float zFar = gContext.mReversed ? 0.f : (1.f - FLT_EPSILON); - - rayOrigin.Transform(makeVect(mox, moy, zNear, 1.f), mViewProjInverse); - rayOrigin *= 1.f / rayOrigin.w; - vec_t rayEnd; - rayEnd.Transform(makeVect(mox, moy, zFar, 1.f), mViewProjInverse); - rayEnd *= 1.f / rayEnd.w; - rayDir = Normalized(rayEnd - rayOrigin); - } - - static float GetSegmentLengthClipSpace(const vec_t& start, const vec_t& end, const bool localCoordinates = false) - { - vec_t startOfSegment = start; - const matrix_t& mvp = localCoordinates ? gContext.mMVPLocal : gContext.mMVP; - startOfSegment.TransformPoint(mvp); - if (fabsf(startOfSegment.w) > FLT_EPSILON) // check for axis aligned with camera direction - { - startOfSegment *= 1.f / startOfSegment.w; - } - - vec_t endOfSegment = end; - endOfSegment.TransformPoint(mvp); - if (fabsf(endOfSegment.w) > FLT_EPSILON) // check for axis aligned with camera direction - { - endOfSegment *= 1.f / endOfSegment.w; - } - - vec_t clipSpaceAxis = endOfSegment - startOfSegment; - if (gContext.mDisplayRatio < 1.0) - clipSpaceAxis.x *= gContext.mDisplayRatio; - else - clipSpaceAxis.y /= gContext.mDisplayRatio; - float segmentLengthInClipSpace = sqrtf(clipSpaceAxis.x * clipSpaceAxis.x + clipSpaceAxis.y * clipSpaceAxis.y); - return segmentLengthInClipSpace; - } - - static float GetParallelogram(const vec_t& ptO, const vec_t& ptA, const vec_t& ptB) - { - vec_t pts[] = { ptO, ptA, ptB }; - for (unsigned int i = 0; i < 3; i++) - { - pts[i].TransformPoint(gContext.mMVP); - if (fabsf(pts[i].w) > FLT_EPSILON) // check for axis aligned with camera direction - { - pts[i] *= 1.f / pts[i].w; - } - } - vec_t segA = pts[1] - pts[0]; - vec_t segB = pts[2] - pts[0]; - segA.y /= gContext.mDisplayRatio; - segB.y /= gContext.mDisplayRatio; - vec_t segAOrtho = makeVect(-segA.y, segA.x); - segAOrtho.Normalize(); - float dt = segAOrtho.Dot3(segB); - float surface = sqrtf(segA.x * segA.x + segA.y * segA.y) * fabsf(dt); - return surface; - } - - inline vec_t PointOnSegment(const vec_t& point, const vec_t& vertPos1, const vec_t& vertPos2) - { - vec_t c = point - vertPos1; - vec_t V; - - V.Normalize(vertPos2 - vertPos1); - float d = (vertPos2 - vertPos1).Length(); - float t = V.Dot3(c); - - if (t < 0.f) - { - return vertPos1; - } - - if (t > d) - { - return vertPos2; - } - - return vertPos1 + V * t; - } - - static float IntersectRayPlane(const vec_t& rOrigin, const vec_t& rVector, const vec_t& plan) - { - const float numer = plan.Dot3(rOrigin) - plan.w; - const float denom = plan.Dot3(rVector); - - if (fabsf(denom) < FLT_EPSILON) // normal is orthogonal to vector, cant intersect - { - return -1.0f; - } - - return -(numer / denom); - } - - static float DistanceToPlane(const vec_t& point, const vec_t& plan) - { - return plan.Dot3(point) + plan.w; - } - - static bool IsInContextRect(ImVec2 p) - { - return IsWithin(p.x, gContext.mX, gContext.mXMax) && IsWithin(p.y, gContext.mY, gContext.mYMax); - } - - static bool IsHoveringWindow() - { - ImGuiContext& g = *ImGui::GetCurrentContext(); - ImGuiWindow* window = ImGui::FindWindowByName(gContext.mDrawList->_OwnerName); - if (g.HoveredWindow == window) // Mouse hovering drawlist window - return true; - if (gContext.mAlternativeWindow != nullptr && g.HoveredWindow == gContext.mAlternativeWindow) - return true; - if (g.HoveredWindow != NULL) // Any other window is hovered - return false; - if (ImGui::IsMouseHoveringRect(window->InnerRect.Min, window->InnerRect.Max, false)) // Hovering drawlist window rect, while no other window is hovered (for _NoInputs windows) - return true; - return false; - } - - void SetRect(float x, float y, float width, float height) - { - gContext.mX = x; - gContext.mY = y; - gContext.mWidth = width; - gContext.mHeight = height; - gContext.mXMax = gContext.mX + gContext.mWidth; - gContext.mYMax = gContext.mY + gContext.mXMax; - gContext.mDisplayRatio = width / height; - } - - void SetOrthographic(bool isOrthographic) - { - gContext.mIsOrthographic = isOrthographic; - } - - void SetDrawlist(ImDrawList* drawlist) - { - gContext.mDrawList = drawlist ? drawlist : ImGui::GetWindowDrawList(); - } - - void SetImGuiContext(ImGuiContext* ctx) - { - ImGui::SetCurrentContext(ctx); - } - - void BeginFrame() - { - const ImU32 flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoBringToFrontOnFocus; + static const float ZPI = 3.14159265358979323846f; + static const float RAD2DEG = (180.f / ZPI); + static const float DEG2RAD = (ZPI / 180.f); + const float screenRotateSize = 0.06f; + // scale a bit so translate axis do not touch when in universal + const float rotationDisplayFactor = 1.2f; + + static OPERATION operator&(OPERATION lhs, OPERATION rhs) + { + return static_cast(static_cast(lhs) & static_cast(rhs)); + } + + static bool operator!=(OPERATION lhs, int rhs) + { + return static_cast(lhs) != rhs; + } + + static bool Intersects(OPERATION lhs, OPERATION rhs) + { + return (lhs & rhs) != 0; + } + + // True if lhs contains rhs + static bool Contains(OPERATION lhs, OPERATION rhs) + { + return (lhs & rhs) == rhs; + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // utility and math + + void FPU_MatrixF_x_MatrixF(const float* a, const float* b, float* r) + { + r[0] = a[0] * b[0] + a[1] * b[4] + a[2] * b[8] + a[3] * b[12]; + r[1] = a[0] * b[1] + a[1] * b[5] + a[2] * b[9] + a[3] * b[13]; + r[2] = a[0] * b[2] + a[1] * b[6] + a[2] * b[10] + a[3] * b[14]; + r[3] = a[0] * b[3] + a[1] * b[7] + a[2] * b[11] + a[3] * b[15]; + + r[4] = a[4] * b[0] + a[5] * b[4] + a[6] * b[8] + a[7] * b[12]; + r[5] = a[4] * b[1] + a[5] * b[5] + a[6] * b[9] + a[7] * b[13]; + r[6] = a[4] * b[2] + a[5] * b[6] + a[6] * b[10] + a[7] * b[14]; + r[7] = a[4] * b[3] + a[5] * b[7] + a[6] * b[11] + a[7] * b[15]; + + r[8] = a[8] * b[0] + a[9] * b[4] + a[10] * b[8] + a[11] * b[12]; + r[9] = a[8] * b[1] + a[9] * b[5] + a[10] * b[9] + a[11] * b[13]; + r[10] = a[8] * b[2] + a[9] * b[6] + a[10] * b[10] + a[11] * b[14]; + r[11] = a[8] * b[3] + a[9] * b[7] + a[10] * b[11] + a[11] * b[15]; + + r[12] = a[12] * b[0] + a[13] * b[4] + a[14] * b[8] + a[15] * b[12]; + r[13] = a[12] * b[1] + a[13] * b[5] + a[14] * b[9] + a[15] * b[13]; + r[14] = a[12] * b[2] + a[13] * b[6] + a[14] * b[10] + a[15] * b[14]; + r[15] = a[12] * b[3] + a[13] * b[7] + a[14] * b[11] + a[15] * b[15]; + } + + void Frustum(float left, float right, float bottom, float top, float znear, float zfar, float* m16) + { + float temp, temp2, temp3, temp4; + temp = 2.0f * znear; + temp2 = right - left; + temp3 = top - bottom; + temp4 = zfar - znear; + m16[0] = temp / temp2; + m16[1] = 0.0; + m16[2] = 0.0; + m16[3] = 0.0; + m16[4] = 0.0; + m16[5] = temp / temp3; + m16[6] = 0.0; + m16[7] = 0.0; + m16[8] = (right + left) / temp2; + m16[9] = (top + bottom) / temp3; + m16[10] = (-zfar - znear) / temp4; + m16[11] = -1.0f; + m16[12] = 0.0; + m16[13] = 0.0; + m16[14] = (-temp * zfar) / temp4; + m16[15] = 0.0; + } + + void Perspective(float fovyInDegrees, float aspectRatio, float znear, float zfar, float* m16) + { + float ymax, xmax; + ymax = znear * tanf(fovyInDegrees * DEG2RAD); + xmax = ymax * aspectRatio; + Frustum(-xmax, xmax, -ymax, ymax, znear, zfar, m16); + } + + void Cross(const float* a, const float* b, float* r) + { + r[0] = a[1] * b[2] - a[2] * b[1]; + r[1] = a[2] * b[0] - a[0] * b[2]; + r[2] = a[0] * b[1] - a[1] * b[0]; + } + + float Dot(const float* a, const float* b) + { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + } + + void Normalize(const float* a, float* r) + { + float il = 1.f / (sqrtf(Dot(a, a)) + FLT_EPSILON); + r[0] = a[0] * il; + r[1] = a[1] * il; + r[2] = a[2] * il; + } + + void LookAt(const float* eye, const float* at, const float* up, float* m16) + { + float X[3], Y[3], Z[3], tmp[3]; + + tmp[0] = eye[0] - at[0]; + tmp[1] = eye[1] - at[1]; + tmp[2] = eye[2] - at[2]; + Normalize(tmp, Z); + Normalize(up, Y); + Cross(Y, Z, tmp); + Normalize(tmp, X); + Cross(Z, X, tmp); + Normalize(tmp, Y); + + m16[0] = X[0]; + m16[1] = Y[0]; + m16[2] = Z[0]; + m16[3] = 0.0f; + m16[4] = X[1]; + m16[5] = Y[1]; + m16[6] = Z[1]; + m16[7] = 0.0f; + m16[8] = X[2]; + m16[9] = Y[2]; + m16[10] = Z[2]; + m16[11] = 0.0f; + m16[12] = -Dot(X, eye); + m16[13] = -Dot(Y, eye); + m16[14] = -Dot(Z, eye); + m16[15] = 1.0f; + } + + template + T Clamp(T x, T y, T z) + { + return ((x < y) ? y : ((x > z) ? z : x)); + } + template + T max(T x, T y) + { + return (x > y) ? x : y; + } + template + T min(T x, T y) + { + return (x < y) ? x : y; + } + template + bool IsWithin(T x, T y, T z) + { + return (x >= y) && (x <= z); + } + + struct matrix_t; + struct vec_t + { + public: + float x, y, z, w; + + void Lerp(const vec_t& v, float t) + { + x += (v.x - x) * t; + y += (v.y - y) * t; + z += (v.z - z) * t; + w += (v.w - w) * t; + } + + void Set(float v) + { + x = y = z = w = v; + } + void Set(float _x, float _y, float _z = 0.f, float _w = 0.f) + { + x = _x; + y = _y; + z = _z; + w = _w; + } + + vec_t& operator-=(const vec_t& v) + { + x -= v.x; + y -= v.y; + z -= v.z; + w -= v.w; + return *this; + } + vec_t& operator+=(const vec_t& v) + { + x += v.x; + y += v.y; + z += v.z; + w += v.w; + return *this; + } + vec_t& operator*=(const vec_t& v) + { + x *= v.x; + y *= v.y; + z *= v.z; + w *= v.w; + return *this; + } + vec_t& operator*=(float v) + { + x *= v; + y *= v; + z *= v; + w *= v; + return *this; + } + + vec_t operator*(float f) const; + vec_t operator-() const; + vec_t operator-(const vec_t& v) const; + vec_t operator+(const vec_t& v) const; + vec_t operator*(const vec_t& v) const; + + const vec_t& operator+() const + { + return (*this); + } + float Length() const + { + return sqrtf(x * x + y * y + z * z); + }; + float LengthSq() const + { + return (x * x + y * y + z * z); + }; + vec_t Normalize() + { + (*this) *= (1.f / (Length() > FLT_EPSILON ? Length() : FLT_EPSILON)); + return (*this); + } + vec_t Normalize(const vec_t& v) + { + this->Set(v.x, v.y, v.z, v.w); + this->Normalize(); + return (*this); + } + vec_t Abs() const; + + void Cross(const vec_t& v) + { + vec_t res; + res.x = y * v.z - z * v.y; + res.y = z * v.x - x * v.z; + res.z = x * v.y - y * v.x; + + x = res.x; + y = res.y; + z = res.z; + w = 0.f; + } + + void Cross(const vec_t& v1, const vec_t& v2) + { + x = v1.y * v2.z - v1.z * v2.y; + y = v1.z * v2.x - v1.x * v2.z; + z = v1.x * v2.y - v1.y * v2.x; + w = 0.f; + } + + float Dot(const vec_t& v) const + { + return (x * v.x) + (y * v.y) + (z * v.z) + (w * v.w); + } + + float Dot3(const vec_t& v) const + { + return (x * v.x) + (y * v.y) + (z * v.z); + } + + void Transform(const matrix_t& matrix); + void Transform(const vec_t& s, const matrix_t& matrix); + + void TransformVector(const matrix_t& matrix); + void TransformPoint(const matrix_t& matrix); + void TransformVector(const vec_t& v, const matrix_t& matrix) + { + (*this) = v; + this->TransformVector(matrix); + } + void TransformPoint(const vec_t& v, const matrix_t& matrix) + { + (*this) = v; + this->TransformPoint(matrix); + } + + float& operator[](size_t index) + { + return ((float*)&x)[index]; + } + const float& operator[](size_t index) const + { + return ((float*)&x)[index]; + } + bool operator!=(const vec_t& other) const + { + return memcmp(this, &other, sizeof(vec_t)) != 0; + } + }; + + vec_t makeVect(float _x, float _y, float _z = 0.f, float _w = 0.f) + { + vec_t res; + res.x = _x; + res.y = _y; + res.z = _z; + res.w = _w; + return res; + } + vec_t makeVect(ImVec2 v) + { + vec_t res; + res.x = v.x; + res.y = v.y; + res.z = 0.f; + res.w = 0.f; + return res; + } + vec_t vec_t::operator*(float f) const + { + return makeVect(x * f, y * f, z * f, w * f); + } + vec_t vec_t::operator-() const + { + return makeVect(-x, -y, -z, -w); + } + vec_t vec_t::operator-(const vec_t& v) const + { + return makeVect(x - v.x, y - v.y, z - v.z, w - v.w); + } + vec_t vec_t::operator+(const vec_t& v) const + { + return makeVect(x + v.x, y + v.y, z + v.z, w + v.w); + } + vec_t vec_t::operator*(const vec_t& v) const + { + return makeVect(x * v.x, y * v.y, z * v.z, w * v.w); + } + vec_t vec_t::Abs() const + { + return makeVect(fabsf(x), fabsf(y), fabsf(z)); + } + + vec_t Normalized(const vec_t& v) + { + vec_t res; + res = v; + res.Normalize(); + return res; + } + vec_t Cross(const vec_t& v1, const vec_t& v2) + { + vec_t res; + res.x = v1.y * v2.z - v1.z * v2.y; + res.y = v1.z * v2.x - v1.x * v2.z; + res.z = v1.x * v2.y - v1.y * v2.x; + res.w = 0.f; + return res; + } + + float Dot(const vec_t& v1, const vec_t& v2) + { + return (v1.x * v2.x) + (v1.y * v2.y) + (v1.z * v2.z); + } + + vec_t BuildPlan(const vec_t& p_point1, const vec_t& p_normal) + { + vec_t normal, res; + normal.Normalize(p_normal); + res.w = normal.Dot(p_point1); + res.x = normal.x; + res.y = normal.y; + res.z = normal.z; + return res; + } + + struct matrix_t + { + public: + union { + float m[4][4]; + float m16[16]; + struct + { + vec_t right, up, dir, position; + } v; + vec_t component[4]; + }; + + operator float*() + { + return m16; + } + operator const float*() const + { + return m16; + } + void Translation(float _x, float _y, float _z) + { + this->Translation(makeVect(_x, _y, _z)); + } + + void Translation(const vec_t& vt) + { + v.right.Set(1.f, 0.f, 0.f, 0.f); + v.up.Set(0.f, 1.f, 0.f, 0.f); + v.dir.Set(0.f, 0.f, 1.f, 0.f); + v.position.Set(vt.x, vt.y, vt.z, 1.f); + } + + void Scale(float _x, float _y, float _z) + { + v.right.Set(_x, 0.f, 0.f, 0.f); + v.up.Set(0.f, _y, 0.f, 0.f); + v.dir.Set(0.f, 0.f, _z, 0.f); + v.position.Set(0.f, 0.f, 0.f, 1.f); + } + void Scale(const vec_t& s) + { + Scale(s.x, s.y, s.z); + } + + matrix_t& operator*=(const matrix_t& mat) + { + matrix_t tmpMat; + tmpMat = *this; + tmpMat.Multiply(mat); + *this = tmpMat; + return *this; + } + matrix_t operator*(const matrix_t& mat) const + { + matrix_t matT; + matT.Multiply(*this, mat); + return matT; + } + + void Multiply(const matrix_t& matrix) + { + matrix_t tmp; + tmp = *this; + + FPU_MatrixF_x_MatrixF((float*)&tmp, (float*)&matrix, (float*)this); + } + + void Multiply(const matrix_t& m1, const matrix_t& m2) + { + FPU_MatrixF_x_MatrixF((float*)&m1, (float*)&m2, (float*)this); + } + + float GetDeterminant() const + { + return m[0][0] * m[1][1] * m[2][2] + m[0][1] * m[1][2] * m[2][0] + m[0][2] * m[1][0] * m[2][1] - m[0][2] * m[1][1] * m[2][0] - + m[0][1] * m[1][0] * m[2][2] - m[0][0] * m[1][2] * m[2][1]; + } + + float Inverse(const matrix_t& srcMatrix, bool affine = false); + void SetToIdentity() + { + v.right.Set(1.f, 0.f, 0.f, 0.f); + v.up.Set(0.f, 1.f, 0.f, 0.f); + v.dir.Set(0.f, 0.f, 1.f, 0.f); + v.position.Set(0.f, 0.f, 0.f, 1.f); + } + void Transpose() + { + matrix_t tmpm; + for (int l = 0; l < 4; l++) + { + for (int c = 0; c < 4; c++) + { + tmpm.m[l][c] = m[c][l]; + } + } + (*this) = tmpm; + } + + void RotationAxis(const vec_t& axis, float angle); + + void OrthoNormalize() + { + v.right.Normalize(); + v.up.Normalize(); + v.dir.Normalize(); + } + }; + + void vec_t::Transform(const matrix_t& matrix) + { + vec_t out; + + out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0] + w * matrix.m[3][0]; + out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1] + w * matrix.m[3][1]; + out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2] + w * matrix.m[3][2]; + out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3] + w * matrix.m[3][3]; + + x = out.x; + y = out.y; + z = out.z; + w = out.w; + } + + void vec_t::Transform(const vec_t& s, const matrix_t& matrix) + { + *this = s; + Transform(matrix); + } + + void vec_t::TransformPoint(const matrix_t& matrix) + { + vec_t out; + + out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0] + matrix.m[3][0]; + out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1] + matrix.m[3][1]; + out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2] + matrix.m[3][2]; + out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3] + matrix.m[3][3]; + + x = out.x; + y = out.y; + z = out.z; + w = out.w; + } + + void vec_t::TransformVector(const matrix_t& matrix) + { + vec_t out; + + out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0]; + out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1]; + out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2]; + out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3]; + + x = out.x; + y = out.y; + z = out.z; + w = out.w; + } + + float matrix_t::Inverse(const matrix_t& srcMatrix, bool affine) + { + float det = 0; + + if (affine) + { + det = GetDeterminant(); + float s = 1 / det; + m[0][0] = (srcMatrix.m[1][1] * srcMatrix.m[2][2] - srcMatrix.m[1][2] * srcMatrix.m[2][1]) * s; + m[0][1] = (srcMatrix.m[2][1] * srcMatrix.m[0][2] - srcMatrix.m[2][2] * srcMatrix.m[0][1]) * s; + m[0][2] = (srcMatrix.m[0][1] * srcMatrix.m[1][2] - srcMatrix.m[0][2] * srcMatrix.m[1][1]) * s; + m[1][0] = (srcMatrix.m[1][2] * srcMatrix.m[2][0] - srcMatrix.m[1][0] * srcMatrix.m[2][2]) * s; + m[1][1] = (srcMatrix.m[2][2] * srcMatrix.m[0][0] - srcMatrix.m[2][0] * srcMatrix.m[0][2]) * s; + m[1][2] = (srcMatrix.m[0][2] * srcMatrix.m[1][0] - srcMatrix.m[0][0] * srcMatrix.m[1][2]) * s; + m[2][0] = (srcMatrix.m[1][0] * srcMatrix.m[2][1] - srcMatrix.m[1][1] * srcMatrix.m[2][0]) * s; + m[2][1] = (srcMatrix.m[2][0] * srcMatrix.m[0][1] - srcMatrix.m[2][1] * srcMatrix.m[0][0]) * s; + m[2][2] = (srcMatrix.m[0][0] * srcMatrix.m[1][1] - srcMatrix.m[0][1] * srcMatrix.m[1][0]) * s; + m[3][0] = -(m[0][0] * srcMatrix.m[3][0] + m[1][0] * srcMatrix.m[3][1] + m[2][0] * srcMatrix.m[3][2]); + m[3][1] = -(m[0][1] * srcMatrix.m[3][0] + m[1][1] * srcMatrix.m[3][1] + m[2][1] * srcMatrix.m[3][2]); + m[3][2] = -(m[0][2] * srcMatrix.m[3][0] + m[1][2] * srcMatrix.m[3][1] + m[2][2] * srcMatrix.m[3][2]); + } + else + { + // transpose matrix + float src[16]; + for (int i = 0; i < 4; ++i) + { + src[i] = srcMatrix.m16[i * 4]; + src[i + 4] = srcMatrix.m16[i * 4 + 1]; + src[i + 8] = srcMatrix.m16[i * 4 + 2]; + src[i + 12] = srcMatrix.m16[i * 4 + 3]; + } + + // calculate pairs for first 8 elements (cofactors) + float tmp[12]; // temp array for pairs + tmp[0] = src[10] * src[15]; + tmp[1] = src[11] * src[14]; + tmp[2] = src[9] * src[15]; + tmp[3] = src[11] * src[13]; + tmp[4] = src[9] * src[14]; + tmp[5] = src[10] * src[13]; + tmp[6] = src[8] * src[15]; + tmp[7] = src[11] * src[12]; + tmp[8] = src[8] * src[14]; + tmp[9] = src[10] * src[12]; + tmp[10] = src[8] * src[13]; + tmp[11] = src[9] * src[12]; + + // calculate first 8 elements (cofactors) + m16[0] = (tmp[0] * src[5] + tmp[3] * src[6] + tmp[4] * src[7]) - (tmp[1] * src[5] + tmp[2] * src[6] + tmp[5] * src[7]); + m16[1] = (tmp[1] * src[4] + tmp[6] * src[6] + tmp[9] * src[7]) - (tmp[0] * src[4] + tmp[7] * src[6] + tmp[8] * src[7]); + m16[2] = (tmp[2] * src[4] + tmp[7] * src[5] + tmp[10] * src[7]) - (tmp[3] * src[4] + tmp[6] * src[5] + tmp[11] * src[7]); + m16[3] = (tmp[5] * src[4] + tmp[8] * src[5] + tmp[11] * src[6]) - (tmp[4] * src[4] + tmp[9] * src[5] + tmp[10] * src[6]); + m16[4] = (tmp[1] * src[1] + tmp[2] * src[2] + tmp[5] * src[3]) - (tmp[0] * src[1] + tmp[3] * src[2] + tmp[4] * src[3]); + m16[5] = (tmp[0] * src[0] + tmp[7] * src[2] + tmp[8] * src[3]) - (tmp[1] * src[0] + tmp[6] * src[2] + tmp[9] * src[3]); + m16[6] = (tmp[3] * src[0] + tmp[6] * src[1] + tmp[11] * src[3]) - (tmp[2] * src[0] + tmp[7] * src[1] + tmp[10] * src[3]); + m16[7] = (tmp[4] * src[0] + tmp[9] * src[1] + tmp[10] * src[2]) - (tmp[5] * src[0] + tmp[8] * src[1] + tmp[11] * src[2]); + + // calculate pairs for second 8 elements (cofactors) + tmp[0] = src[2] * src[7]; + tmp[1] = src[3] * src[6]; + tmp[2] = src[1] * src[7]; + tmp[3] = src[3] * src[5]; + tmp[4] = src[1] * src[6]; + tmp[5] = src[2] * src[5]; + tmp[6] = src[0] * src[7]; + tmp[7] = src[3] * src[4]; + tmp[8] = src[0] * src[6]; + tmp[9] = src[2] * src[4]; + tmp[10] = src[0] * src[5]; + tmp[11] = src[1] * src[4]; + + // calculate second 8 elements (cofactors) + m16[8] = (tmp[0] * src[13] + tmp[3] * src[14] + tmp[4] * src[15]) - (tmp[1] * src[13] + tmp[2] * src[14] + tmp[5] * src[15]); + m16[9] = (tmp[1] * src[12] + tmp[6] * src[14] + tmp[9] * src[15]) - (tmp[0] * src[12] + tmp[7] * src[14] + tmp[8] * src[15]); + m16[10] = (tmp[2] * src[12] + tmp[7] * src[13] + tmp[10] * src[15]) - (tmp[3] * src[12] + tmp[6] * src[13] + tmp[11] * src[15]); + m16[11] = (tmp[5] * src[12] + tmp[8] * src[13] + tmp[11] * src[14]) - (tmp[4] * src[12] + tmp[9] * src[13] + tmp[10] * src[14]); + m16[12] = (tmp[2] * src[10] + tmp[5] * src[11] + tmp[1] * src[9]) - (tmp[4] * src[11] + tmp[0] * src[9] + tmp[3] * src[10]); + m16[13] = (tmp[8] * src[11] + tmp[0] * src[8] + tmp[7] * src[10]) - (tmp[6] * src[10] + tmp[9] * src[11] + tmp[1] * src[8]); + m16[14] = (tmp[6] * src[9] + tmp[11] * src[11] + tmp[3] * src[8]) - (tmp[10] * src[11] + tmp[2] * src[8] + tmp[7] * src[9]); + m16[15] = (tmp[10] * src[10] + tmp[4] * src[8] + tmp[9] * src[9]) - (tmp[8] * src[9] + tmp[11] * src[10] + tmp[5] * src[8]); + + // calculate determinant + det = src[0] * m16[0] + src[1] * m16[1] + src[2] * m16[2] + src[3] * m16[3]; + + // calculate matrix inverse + float invdet = 1 / det; + for (int j = 0; j < 16; ++j) + { + m16[j] *= invdet; + } + } + + return det; + } + + void matrix_t::RotationAxis(const vec_t& axis, float angle) + { + float length2 = axis.LengthSq(); + if (length2 < FLT_EPSILON) + { + SetToIdentity(); + return; + } + + vec_t n = axis * (1.f / sqrtf(length2)); + float s = sinf(angle); + float c = cosf(angle); + float k = 1.f - c; + + float xx = n.x * n.x * k + c; + float yy = n.y * n.y * k + c; + float zz = n.z * n.z * k + c; + float xy = n.x * n.y * k; + float yz = n.y * n.z * k; + float zx = n.z * n.x * k; + float xs = n.x * s; + float ys = n.y * s; + float zs = n.z * s; + + m[0][0] = xx; + m[0][1] = xy + zs; + m[0][2] = zx - ys; + m[0][3] = 0.f; + m[1][0] = xy - zs; + m[1][1] = yy; + m[1][2] = yz + xs; + m[1][3] = 0.f; + m[2][0] = zx + ys; + m[2][1] = yz - xs; + m[2][2] = zz; + m[2][3] = 0.f; + m[3][0] = 0.f; + m[3][1] = 0.f; + m[3][2] = 0.f; + m[3][3] = 1.f; + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + + enum MOVETYPE + { + MT_NONE, + MT_MOVE_X, + MT_MOVE_Y, + MT_MOVE_Z, + MT_MOVE_YZ, + MT_MOVE_ZX, + MT_MOVE_XY, + MT_MOVE_SCREEN, + MT_ROTATE_X, + MT_ROTATE_Y, + MT_ROTATE_Z, + MT_ROTATE_SCREEN, + MT_SCALE_X, + MT_SCALE_Y, + MT_SCALE_Z, + MT_SCALE_XYZ + }; + + static bool IsTranslateType(int type) + { + return type >= MT_MOVE_X && type <= MT_MOVE_SCREEN; + } + + static bool IsRotateType(int type) + { + return type >= MT_ROTATE_X && type <= MT_ROTATE_SCREEN; + } + + static bool IsScaleType(int type) + { + return type >= MT_SCALE_X && type <= MT_SCALE_XYZ; + } + + // Matches MT_MOVE_AB order + static const OPERATION TRANSLATE_PLANS[3] = { TRANSLATE_Y | TRANSLATE_Z, TRANSLATE_X | TRANSLATE_Z, TRANSLATE_X | TRANSLATE_Y }; + + Style::Style() + { + // default values + TranslationLineThickness = 3.0f; + TranslationLineArrowSize = 6.0f; + RotationLineThickness = 2.0f; + RotationOuterLineThickness = 3.0f; + ScaleLineThickness = 3.0f; + ScaleLineCircleSize = 6.0f; + HatchedAxisLineThickness = 6.0f; + CenterCircleSize = 6.0f; + + // initialize default colors + Colors[DIRECTION_X] = ImVec4(0.666f, 0.000f, 0.000f, 1.000f); + Colors[DIRECTION_Y] = ImVec4(0.000f, 0.666f, 0.000f, 1.000f); + Colors[DIRECTION_Z] = ImVec4(0.000f, 0.000f, 0.666f, 1.000f); + Colors[PLANE_X] = ImVec4(0.666f, 0.000f, 0.000f, 0.380f); + Colors[PLANE_Y] = ImVec4(0.000f, 0.666f, 0.000f, 0.380f); + Colors[PLANE_Z] = ImVec4(0.000f, 0.000f, 0.666f, 0.380f); + Colors[SELECTION] = ImVec4(1.000f, 0.500f, 0.062f, 0.541f); + Colors[INACTIVE] = ImVec4(0.600f, 0.600f, 0.600f, 0.600f); + Colors[TRANSLATION_LINE] = ImVec4(0.666f, 0.666f, 0.666f, 0.666f); + Colors[SCALE_LINE] = ImVec4(0.250f, 0.250f, 0.250f, 1.000f); + Colors[ROTATION_USING_BORDER] = ImVec4(1.000f, 0.500f, 0.062f, 1.000f); + Colors[ROTATION_USING_FILL] = ImVec4(1.000f, 0.500f, 0.062f, 0.500f); + Colors[HATCHED_AXIS_LINES] = ImVec4(0.000f, 0.000f, 0.000f, 0.500f); + Colors[TEXT] = ImVec4(1.000f, 1.000f, 1.000f, 1.000f); + Colors[TEXT_SHADOW] = ImVec4(0.000f, 0.000f, 0.000f, 1.000f); + } + + struct Context + { + Context() + : mbUsing(false) + , mbUsingViewManipulate(false) + , mbEnable(true) + , mIsViewManipulatorHovered(false) + , mbUsingBounds(false) + { + } + + ImDrawList* mDrawList; + Style mStyle; + + MODE mMode; + matrix_t mViewMat; + matrix_t mProjectionMat; + matrix_t mModel; + matrix_t mModelLocal; // orthonormalized model + matrix_t mModelInverse; + matrix_t mModelSource; + matrix_t mModelSourceInverse; + matrix_t mMVP; + matrix_t + mMVPLocal; // MVP with full model matrix whereas mMVP's model matrix might only be translation in case of World space edition + matrix_t mViewProjection; + + vec_t mModelScaleOrigin; + vec_t mCameraEye; + vec_t mCameraRight; + vec_t mCameraDir; + vec_t mCameraUp; + vec_t mRayOrigin; + vec_t mRayVector; + + float mRadiusSquareCenter; + ImVec2 mScreenSquareCenter; + ImVec2 mScreenSquareMin; + ImVec2 mScreenSquareMax; + + float mScreenFactor; + vec_t mRelativeOrigin; + + bool mbUsing; + bool mbUsingViewManipulate; + bool mbEnable; + bool mbMouseOver; + bool mReversed; // reversed projection matrix + bool mIsViewManipulatorHovered; + + // translation + vec_t mTranslationPlan; + vec_t mTranslationPlanOrigin; + vec_t mMatrixOrigin; + vec_t mTranslationLastDelta; + + // rotation + vec_t mRotationVectorSource; + float mRotationAngle; + float mRotationAngleOrigin; + // vec_t mWorldToLocalAxis; + + // scale + vec_t mScale; + vec_t mScaleValueOrigin; + vec_t mScaleLast; + float mSaveMousePosx; + + // save axis factor when using gizmo + bool mBelowAxisLimit[3]; + int mAxisMask = 0; + bool mBelowPlaneLimit[3]; + float mAxisFactor[3]; + + float mAxisLimit = 0.0025f; + float mPlaneLimit = 0.02f; + + // bounds stretching + vec_t mBoundsPivot; + vec_t mBoundsAnchor; + vec_t mBoundsPlan; + vec_t mBoundsLocalPivot; + int mBoundsBestAxis; + int mBoundsAxis[2]; + bool mbUsingBounds; + matrix_t mBoundsMatrix; + + // + int mCurrentOperation; + + float mX = 0.f; + float mY = 0.f; + float mWidth = 0.f; + float mHeight = 0.f; + float mXMax = 0.f; + float mYMax = 0.f; + float mDisplayRatio = 1.f; + + bool mIsOrthographic = false; + // check to not have multiple gizmo highlighted at the same time + bool mbOverGizmoHotspot = false; + + ImGuiWindow* mAlternativeWindow = nullptr; + ImVector mIDStack; + ImGuiID mEditingID = -1; + OPERATION mOperation = OPERATION(-1); + + bool mAllowAxisFlip = true; + float mGizmoSizeClipSpace = 0.1f; + + inline ImGuiID GetCurrentID() + { + if (mIDStack.empty()) + { + mIDStack.push_back(-1); + } + return mIDStack.back(); + } + }; + + static Context gContext; + + static const vec_t directionUnary[3] = { makeVect(1.f, 0.f, 0.f), makeVect(0.f, 1.f, 0.f), makeVect(0.f, 0.f, 1.f) }; + static const char* translationInfoMask[] = { "X : %5.3f", + "Y : %5.3f", + "Z : %5.3f", + "Y : %5.3f Z : %5.3f", + "X : %5.3f Z : %5.3f", + "X : %5.3f Y : %5.3f", + "X : %5.3f Y : %5.3f Z : %5.3f" }; + static const char* scaleInfoMask[] = { "X : %5.2f", "Y : %5.2f", "Z : %5.2f", "XYZ : %5.2f" }; + static const char* rotationInfoMask[] = { + "X : %5.2f deg %5.2f rad", "Y : %5.2f deg %5.2f rad", "Z : %5.2f deg %5.2f rad", "Screen : %5.2f deg %5.2f rad" + }; + static const int translationInfoIndex[] = { 0, 0, 0, 1, 0, 0, 2, 0, 0, 1, 2, 0, 0, 2, 0, 0, 1, 0, 0, 1, 2 }; + static const float quadMin = 0.5f; + static const float quadMax = 0.8f; + static const float quadUV[8] = { quadMin, quadMin, quadMin, quadMax, quadMax, quadMax, quadMax, quadMin }; + static const int halfCircleSegmentCount = 64; + static const float snapTension = 0.5f; + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + static int GetMoveType(OPERATION op, vec_t* gizmoHitProportion); + static int GetRotateType(OPERATION op); + static int GetScaleType(OPERATION op); + + Style& GetStyle() + { + return gContext.mStyle; + } + + static ImU32 GetColorU32(int idx) + { + IM_ASSERT(idx < COLOR::COUNT); + return ImGui::ColorConvertFloat4ToU32(gContext.mStyle.Colors[idx]); + } + + static ImVec2 worldToPos( + const vec_t& worldPos, + const matrix_t& mat, + ImVec2 position = ImVec2(gContext.mX, gContext.mY), + ImVec2 size = ImVec2(gContext.mWidth, gContext.mHeight)) + { + vec_t trans; + trans.TransformPoint(worldPos, mat); + trans *= 0.5f / trans.w; + trans += makeVect(0.5f, 0.5f); + trans.y = 1.f - trans.y; + trans.x *= size.x; + trans.y *= size.y; + trans.x += position.x; + trans.y += position.y; + return ImVec2(trans.x, trans.y); + } + + static void ComputeCameraRay( + vec_t& rayOrigin, + vec_t& rayDir, + ImVec2 position = ImVec2(gContext.mX, gContext.mY), + ImVec2 size = ImVec2(gContext.mWidth, gContext.mHeight)) + { + ImGuiIO& io = ImGui::GetIO(); + + matrix_t mViewProjInverse; + mViewProjInverse.Inverse(gContext.mViewMat * gContext.mProjectionMat); + + const float mox = ((io.MousePos.x - position.x) / size.x) * 2.f - 1.f; + const float moy = (1.f - ((io.MousePos.y - position.y) / size.y)) * 2.f - 1.f; + + const float zNear = gContext.mReversed ? (1.f - FLT_EPSILON) : 0.f; + const float zFar = gContext.mReversed ? 0.f : (1.f - FLT_EPSILON); + + rayOrigin.Transform(makeVect(mox, moy, zNear, 1.f), mViewProjInverse); + rayOrigin *= 1.f / rayOrigin.w; + vec_t rayEnd; + rayEnd.Transform(makeVect(mox, moy, zFar, 1.f), mViewProjInverse); + rayEnd *= 1.f / rayEnd.w; + rayDir = Normalized(rayEnd - rayOrigin); + } + + static float GetSegmentLengthClipSpace(const vec_t& start, const vec_t& end, const bool localCoordinates = false) + { + vec_t startOfSegment = start; + const matrix_t& mvp = localCoordinates ? gContext.mMVPLocal : gContext.mMVP; + startOfSegment.TransformPoint(mvp); + if (fabsf(startOfSegment.w) > FLT_EPSILON) // check for axis aligned with camera direction + { + startOfSegment *= 1.f / startOfSegment.w; + } + + vec_t endOfSegment = end; + endOfSegment.TransformPoint(mvp); + if (fabsf(endOfSegment.w) > FLT_EPSILON) // check for axis aligned with camera direction + { + endOfSegment *= 1.f / endOfSegment.w; + } + + vec_t clipSpaceAxis = endOfSegment - startOfSegment; + if (gContext.mDisplayRatio < 1.0) + clipSpaceAxis.x *= gContext.mDisplayRatio; + else + clipSpaceAxis.y /= gContext.mDisplayRatio; + float segmentLengthInClipSpace = sqrtf(clipSpaceAxis.x * clipSpaceAxis.x + clipSpaceAxis.y * clipSpaceAxis.y); + return segmentLengthInClipSpace; + } + + static float GetParallelogram(const vec_t& ptO, const vec_t& ptA, const vec_t& ptB) + { + vec_t pts[] = { ptO, ptA, ptB }; + for (unsigned int i = 0; i < 3; i++) + { + pts[i].TransformPoint(gContext.mMVP); + if (fabsf(pts[i].w) > FLT_EPSILON) // check for axis aligned with camera direction + { + pts[i] *= 1.f / pts[i].w; + } + } + vec_t segA = pts[1] - pts[0]; + vec_t segB = pts[2] - pts[0]; + segA.y /= gContext.mDisplayRatio; + segB.y /= gContext.mDisplayRatio; + vec_t segAOrtho = makeVect(-segA.y, segA.x); + segAOrtho.Normalize(); + float dt = segAOrtho.Dot3(segB); + float surface = sqrtf(segA.x * segA.x + segA.y * segA.y) * fabsf(dt); + return surface; + } + + inline vec_t PointOnSegment(const vec_t& point, const vec_t& vertPos1, const vec_t& vertPos2) + { + vec_t c = point - vertPos1; + vec_t V; + + V.Normalize(vertPos2 - vertPos1); + float d = (vertPos2 - vertPos1).Length(); + float t = V.Dot3(c); + + if (t < 0.f) + { + return vertPos1; + } + + if (t > d) + { + return vertPos2; + } + + return vertPos1 + V * t; + } + + static float IntersectRayPlane(const vec_t& rOrigin, const vec_t& rVector, const vec_t& plan) + { + const float numer = plan.Dot3(rOrigin) - plan.w; + const float denom = plan.Dot3(rVector); + + if (fabsf(denom) < FLT_EPSILON) // normal is orthogonal to vector, cant intersect + { + return -1.0f; + } + + return -(numer / denom); + } + + static float DistanceToPlane(const vec_t& point, const vec_t& plan) + { + return plan.Dot3(point) + plan.w; + } + + static bool IsInContextRect(ImVec2 p) + { + return IsWithin(p.x, gContext.mX, gContext.mXMax) && IsWithin(p.y, gContext.mY, gContext.mYMax); + } + + static bool IsHoveringWindow() + { + ImGuiContext& g = *ImGui::GetCurrentContext(); + ImGuiWindow* window = ImGui::FindWindowByName(gContext.mDrawList->_OwnerName); + if (g.HoveredWindow == window) // Mouse hovering drawlist window + return true; + if (gContext.mAlternativeWindow != nullptr && g.HoveredWindow == gContext.mAlternativeWindow) + return true; + if (g.HoveredWindow != NULL) // Any other window is hovered + return false; + if (ImGui::IsMouseHoveringRect( + window->InnerRect.Min, + window->InnerRect.Max, + false)) // Hovering drawlist window rect, while no other window is hovered (for _NoInputs windows) + return true; + return false; + } + + void SetRect(float x, float y, float width, float height) + { + gContext.mX = x; + gContext.mY = y; + gContext.mWidth = width; + gContext.mHeight = height; + gContext.mXMax = gContext.mX + gContext.mWidth; + gContext.mYMax = gContext.mY + gContext.mXMax; + gContext.mDisplayRatio = width / height; + } + + void SetOrthographic(bool isOrthographic) + { + gContext.mIsOrthographic = isOrthographic; + } + + void SetDrawlist(ImDrawList* drawlist) + { + gContext.mDrawList = drawlist ? drawlist : ImGui::GetWindowDrawList(); + } + + void SetImGuiContext(ImGuiContext* ctx) + { + ImGui::SetCurrentContext(ctx); + } + + void BeginFrame() + { + const ImU32 flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoBringToFrontOnFocus; #ifdef IMGUI_HAS_VIEWPORT - ImGui::SetNextWindowSize(ImGui::GetMainViewport()->Size); - ImGui::SetNextWindowPos(ImGui::GetMainViewport()->Pos); + ImGui::SetNextWindowSize(ImGui::GetMainViewport()->Size); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->Pos); #else - ImGuiIO& io = ImGui::GetIO(); - ImGui::SetNextWindowSize(io.DisplaySize); - ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGuiIO& io = ImGui::GetIO(); + ImGui::SetNextWindowSize(io.DisplaySize); + ImGui::SetNextWindowPos(ImVec2(0, 0)); #endif - ImGui::PushStyleColor(ImGuiCol_WindowBg, 0); - ImGui::PushStyleColor(ImGuiCol_Border, 0); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - - ImGui::Begin("gizmo", NULL, flags); - gContext.mDrawList = ImGui::GetWindowDrawList(); - gContext.mbOverGizmoHotspot = false; - ImGui::End(); - ImGui::PopStyleVar(); - ImGui::PopStyleColor(2); - } - - bool IsUsing() - { - return (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) || gContext.mbUsingBounds; - } - - bool IsUsingViewManipulate() - { - return gContext.mbUsingViewManipulate; - } - - bool IsViewManipulateHovered() - { - return gContext.mIsViewManipulatorHovered; - } - - bool IsUsingAny() - { - return gContext.mbUsing || gContext.mbUsingBounds; - } - - bool IsOver() - { - return (Intersects(gContext.mOperation, TRANSLATE) && GetMoveType(gContext.mOperation, NULL) != MT_NONE) || - (Intersects(gContext.mOperation, ROTATE) && GetRotateType(gContext.mOperation) != MT_NONE) || - (Intersects(gContext.mOperation, SCALE) && GetScaleType(gContext.mOperation) != MT_NONE) || IsUsing(); - } - - bool IsOver(OPERATION op) - { - if(IsUsing()) - { - return true; - } - if(Intersects(op, SCALE) && GetScaleType(op) != MT_NONE) - { - return true; - } - if(Intersects(op, ROTATE) && GetRotateType(op) != MT_NONE) - { - return true; - } - if(Intersects(op, TRANSLATE) && GetMoveType(op, NULL) != MT_NONE) - { - return true; - } - return false; - } - - void Enable(bool enable) - { - gContext.mbEnable = enable; - if (!enable) - { - gContext.mbUsing = false; - gContext.mbUsingBounds = false; - } - } - - static void ComputeContext(const float* view, const float* projection, float* matrix, MODE mode) - { - gContext.mMode = mode; - gContext.mViewMat = *(matrix_t*)view; - gContext.mProjectionMat = *(matrix_t*)projection; - gContext.mbMouseOver = IsHoveringWindow(); - - gContext.mModelLocal = *(matrix_t*)matrix; - gContext.mModelLocal.OrthoNormalize(); - - if (mode == LOCAL) - { - gContext.mModel = gContext.mModelLocal; - } - else - { - gContext.mModel.Translation(((matrix_t*)matrix)->v.position); - } - gContext.mModelSource = *(matrix_t*)matrix; - gContext.mModelScaleOrigin.Set(gContext.mModelSource.v.right.Length(), gContext.mModelSource.v.up.Length(), gContext.mModelSource.v.dir.Length()); - - gContext.mModelInverse.Inverse(gContext.mModel); - gContext.mModelSourceInverse.Inverse(gContext.mModelSource); - gContext.mViewProjection = gContext.mViewMat * gContext.mProjectionMat; - gContext.mMVP = gContext.mModel * gContext.mViewProjection; - gContext.mMVPLocal = gContext.mModelLocal * gContext.mViewProjection; - - matrix_t viewInverse; - viewInverse.Inverse(gContext.mViewMat); - gContext.mCameraDir = viewInverse.v.dir; - gContext.mCameraEye = viewInverse.v.position; - gContext.mCameraRight = viewInverse.v.right; - gContext.mCameraUp = viewInverse.v.up; - - // projection reverse - vec_t nearPos, farPos; - nearPos.Transform(makeVect(0, 0, 1.f, 1.f), gContext.mProjectionMat); - farPos.Transform(makeVect(0, 0, 2.f, 1.f), gContext.mProjectionMat); - - gContext.mReversed = (nearPos.z/nearPos.w) > (farPos.z / farPos.w); - - // compute scale from the size of camera right vector projected on screen at the matrix position - vec_t pointRight = viewInverse.v.right; - pointRight.TransformPoint(gContext.mViewProjection); - - vec_t rightViewInverse = viewInverse.v.right; - rightViewInverse.TransformVector(gContext.mModelInverse); - float rightLength = GetSegmentLengthClipSpace(makeVect(0.f, 0.f), rightViewInverse); - gContext.mScreenFactor = gContext.mGizmoSizeClipSpace / rightLength; - - ImVec2 centerSSpace = worldToPos(makeVect(0.f, 0.f), gContext.mMVP); - gContext.mScreenSquareCenter = centerSSpace; - gContext.mScreenSquareMin = ImVec2(centerSSpace.x - 10.f, centerSSpace.y - 10.f); - gContext.mScreenSquareMax = ImVec2(centerSSpace.x + 10.f, centerSSpace.y + 10.f); - - ComputeCameraRay(gContext.mRayOrigin, gContext.mRayVector); - } - - static void ComputeColors(ImU32* colors, int type, OPERATION operation) - { - if (gContext.mbEnable) - { - ImU32 selectionColor = GetColorU32(SELECTION); - - switch (operation) - { - case TRANSLATE: - colors[0] = (type == MT_MOVE_SCREEN) ? selectionColor : IM_COL32_WHITE; - for (int i = 0; i < 3; i++) + ImGui::PushStyleColor(ImGuiCol_WindowBg, 0); + ImGui::PushStyleColor(ImGuiCol_Border, 0); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + + ImGui::Begin("gizmo", NULL, flags); + gContext.mDrawList = ImGui::GetWindowDrawList(); + gContext.mbOverGizmoHotspot = false; + ImGui::End(); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + } + + bool IsUsing() + { + return (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) || gContext.mbUsingBounds; + } + + bool IsUsingViewManipulate() + { + return gContext.mbUsingViewManipulate; + } + + bool IsViewManipulateHovered() + { + return gContext.mIsViewManipulatorHovered; + } + + bool IsUsingAny() + { + return gContext.mbUsing || gContext.mbUsingBounds; + } + + bool IsOver() + { + return (Intersects(gContext.mOperation, TRANSLATE) && GetMoveType(gContext.mOperation, NULL) != MT_NONE) || + (Intersects(gContext.mOperation, ROTATE) && GetRotateType(gContext.mOperation) != MT_NONE) || + (Intersects(gContext.mOperation, SCALE) && GetScaleType(gContext.mOperation) != MT_NONE) || IsUsing(); + } + + bool IsOver(OPERATION op) + { + if (IsUsing()) + { + return true; + } + if (Intersects(op, SCALE) && GetScaleType(op) != MT_NONE) + { + return true; + } + if (Intersects(op, ROTATE) && GetRotateType(op) != MT_NONE) + { + return true; + } + if (Intersects(op, TRANSLATE) && GetMoveType(op, NULL) != MT_NONE) + { + return true; + } + return false; + } + + void Enable(bool enable) + { + gContext.mbEnable = enable; + if (!enable) + { + gContext.mbUsing = false; + gContext.mbUsingBounds = false; + } + } + + static void ComputeContext(const float* view, const float* projection, float* matrix, MODE mode) + { + gContext.mMode = mode; + gContext.mViewMat = *(matrix_t*)view; + gContext.mProjectionMat = *(matrix_t*)projection; + gContext.mbMouseOver = IsHoveringWindow(); + + gContext.mModelLocal = *(matrix_t*)matrix; + gContext.mModelLocal.OrthoNormalize(); + + if (mode == LOCAL) + { + gContext.mModel = gContext.mModelLocal; + } + else + { + gContext.mModel.Translation(((matrix_t*)matrix)->v.position); + } + gContext.mModelSource = *(matrix_t*)matrix; + gContext.mModelScaleOrigin.Set( + gContext.mModelSource.v.right.Length(), gContext.mModelSource.v.up.Length(), gContext.mModelSource.v.dir.Length()); + + gContext.mModelInverse.Inverse(gContext.mModel); + gContext.mModelSourceInverse.Inverse(gContext.mModelSource); + gContext.mViewProjection = gContext.mViewMat * gContext.mProjectionMat; + gContext.mMVP = gContext.mModel * gContext.mViewProjection; + gContext.mMVPLocal = gContext.mModelLocal * gContext.mViewProjection; + + matrix_t viewInverse; + viewInverse.Inverse(gContext.mViewMat); + gContext.mCameraDir = viewInverse.v.dir; + gContext.mCameraEye = viewInverse.v.position; + gContext.mCameraRight = viewInverse.v.right; + gContext.mCameraUp = viewInverse.v.up; + + // projection reverse + vec_t nearPos, farPos; + nearPos.Transform(makeVect(0, 0, 1.f, 1.f), gContext.mProjectionMat); + farPos.Transform(makeVect(0, 0, 2.f, 1.f), gContext.mProjectionMat); + + gContext.mReversed = (nearPos.z / nearPos.w) > (farPos.z / farPos.w); + + // compute scale from the size of camera right vector projected on screen at the matrix position + vec_t pointRight = viewInverse.v.right; + pointRight.TransformPoint(gContext.mViewProjection); + + vec_t rightViewInverse = viewInverse.v.right; + rightViewInverse.TransformVector(gContext.mModelInverse); + float rightLength = GetSegmentLengthClipSpace(makeVect(0.f, 0.f), rightViewInverse); + gContext.mScreenFactor = gContext.mGizmoSizeClipSpace / rightLength; + + ImVec2 centerSSpace = worldToPos(makeVect(0.f, 0.f), gContext.mMVP); + gContext.mScreenSquareCenter = centerSSpace; + gContext.mScreenSquareMin = ImVec2(centerSSpace.x - 10.f, centerSSpace.y - 10.f); + gContext.mScreenSquareMax = ImVec2(centerSSpace.x + 10.f, centerSSpace.y + 10.f); + + ComputeCameraRay(gContext.mRayOrigin, gContext.mRayVector); + } + + static void ComputeColors(ImU32* colors, int type, OPERATION operation) + { + if (gContext.mbEnable) + { + ImU32 selectionColor = GetColorU32(SELECTION); + + switch (operation) { - colors[i + 1] = (type == (int)(MT_MOVE_X + i)) ? selectionColor : GetColorU32(DIRECTION_X + i); - colors[i + 4] = (type == (int)(MT_MOVE_YZ + i)) ? selectionColor : GetColorU32(PLANE_X + i); - colors[i + 4] = (type == MT_MOVE_SCREEN) ? selectionColor : colors[i + 4]; + case TRANSLATE: + colors[0] = (type == MT_MOVE_SCREEN) ? selectionColor : IM_COL32_WHITE; + for (int i = 0; i < 3; i++) + { + colors[i + 1] = (type == (int)(MT_MOVE_X + i)) ? selectionColor : GetColorU32(DIRECTION_X + i); + colors[i + 4] = (type == (int)(MT_MOVE_YZ + i)) ? selectionColor : GetColorU32(PLANE_X + i); + colors[i + 4] = (type == MT_MOVE_SCREEN) ? selectionColor : colors[i + 4]; + } + break; + case ROTATE: + colors[0] = (type == MT_ROTATE_SCREEN) ? selectionColor : IM_COL32_WHITE; + for (int i = 0; i < 3; i++) + { + colors[i + 1] = (type == (int)(MT_ROTATE_X + i)) ? selectionColor : GetColorU32(DIRECTION_X + i); + } + break; + case SCALEU: + case SCALE: + colors[0] = (type == MT_SCALE_XYZ) ? selectionColor : IM_COL32_WHITE; + for (int i = 0; i < 3; i++) + { + colors[i + 1] = (type == (int)(MT_SCALE_X + i)) ? selectionColor : GetColorU32(DIRECTION_X + i); + } + break; + // note: this internal function is only called with three possible values for operation + default: + break; } - break; - case ROTATE: - colors[0] = (type == MT_ROTATE_SCREEN) ? selectionColor : IM_COL32_WHITE; - for (int i = 0; i < 3; i++) + } + else + { + ImU32 inactiveColor = GetColorU32(INACTIVE); + for (int i = 0; i < 7; i++) { - colors[i + 1] = (type == (int)(MT_ROTATE_X + i)) ? selectionColor : GetColorU32(DIRECTION_X + i); + colors[i] = inactiveColor; } - break; - case SCALEU: - case SCALE: - colors[0] = (type == MT_SCALE_XYZ) ? selectionColor : IM_COL32_WHITE; - for (int i = 0; i < 3; i++) + } + } + + static void ComputeTripodAxisAndVisibility( + const int axisIndex, + vec_t& dirAxis, + vec_t& dirPlaneX, + vec_t& dirPlaneY, + bool& belowAxisLimit, + bool& belowPlaneLimit, + const bool localCoordinates = false) + { + dirAxis = directionUnary[axisIndex]; + dirPlaneX = directionUnary[(axisIndex + 1) % 3]; + dirPlaneY = directionUnary[(axisIndex + 2) % 3]; + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) + { + // when using, use stored factors so the gizmo doesn't flip when we translate + + // Apply axis mask to axes and planes + belowAxisLimit = gContext.mBelowAxisLimit[axisIndex] && ((1 << axisIndex) & gContext.mAxisMask); + belowPlaneLimit = gContext.mBelowPlaneLimit[axisIndex] && (((1 << axisIndex) == gContext.mAxisMask) || !gContext.mAxisMask); + + dirAxis *= gContext.mAxisFactor[axisIndex]; + dirPlaneX *= gContext.mAxisFactor[(axisIndex + 1) % 3]; + dirPlaneY *= gContext.mAxisFactor[(axisIndex + 2) % 3]; + } + else + { + // new method + float lenDir = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirAxis, localCoordinates); + float lenDirMinus = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), -dirAxis, localCoordinates); + + float lenDirPlaneX = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirPlaneX, localCoordinates); + float lenDirMinusPlaneX = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), -dirPlaneX, localCoordinates); + + float lenDirPlaneY = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirPlaneY, localCoordinates); + float lenDirMinusPlaneY = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), -dirPlaneY, localCoordinates); + + // For readability + bool& allowFlip = gContext.mAllowAxisFlip; + float mulAxis = (allowFlip && lenDir < lenDirMinus && fabsf(lenDir - lenDirMinus) > FLT_EPSILON) ? -1.f : 1.f; + float mulAxisX = + (allowFlip && lenDirPlaneX < lenDirMinusPlaneX && fabsf(lenDirPlaneX - lenDirMinusPlaneX) > FLT_EPSILON) ? -1.f : 1.f; + float mulAxisY = + (allowFlip && lenDirPlaneY < lenDirMinusPlaneY && fabsf(lenDirPlaneY - lenDirMinusPlaneY) > FLT_EPSILON) ? -1.f : 1.f; + dirAxis *= mulAxis; + dirPlaneX *= mulAxisX; + dirPlaneY *= mulAxisY; + + // for axis + float axisLengthInClipSpace = + GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirAxis * gContext.mScreenFactor, localCoordinates); + + float paraSurf = + GetParallelogram(makeVect(0.f, 0.f, 0.f), dirPlaneX * gContext.mScreenFactor, dirPlaneY * gContext.mScreenFactor); + // Apply axis mask to axes and planes + belowPlaneLimit = (paraSurf > gContext.mAxisLimit) && (((1 << axisIndex) == gContext.mAxisMask) || !gContext.mAxisMask); + belowAxisLimit = (axisLengthInClipSpace > gContext.mPlaneLimit) && !((1 << axisIndex) & gContext.mAxisMask); + + // and store values + gContext.mAxisFactor[axisIndex] = mulAxis; + gContext.mAxisFactor[(axisIndex + 1) % 3] = mulAxisX; + gContext.mAxisFactor[(axisIndex + 2) % 3] = mulAxisY; + gContext.mBelowAxisLimit[axisIndex] = belowAxisLimit; + gContext.mBelowPlaneLimit[axisIndex] = belowPlaneLimit; + } + } + + static void ComputeSnap(float* value, float snap) + { + if (snap <= FLT_EPSILON) + { + return; + } + + float modulo = fmodf(*value, snap); + float moduloRatio = fabsf(modulo) / snap; + if (moduloRatio < snapTension) + { + *value -= modulo; + } + else if (moduloRatio > (1.f - snapTension)) + { + *value = *value - modulo + snap * ((*value < 0.f) ? -1.f : 1.f); + } + } + static void ComputeSnap(vec_t& value, const float* snap) + { + for (int i = 0; i < 3; i++) + { + ComputeSnap(&value[i], snap[i]); + } + } + + static float ComputeAngleOnPlan() + { + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + vec_t localPos = Normalized(gContext.mRayOrigin + gContext.mRayVector * len - gContext.mModel.v.position); + + vec_t perpendicularVector; + perpendicularVector.Cross(gContext.mRotationVectorSource, gContext.mTranslationPlan); + perpendicularVector.Normalize(); + float acosAngle = Clamp(Dot(localPos, gContext.mRotationVectorSource), -1.f, 1.f); + float angle = acosf(acosAngle); + angle *= (Dot(localPos, perpendicularVector) < 0.f) ? 1.f : -1.f; + return angle; + } + + static void DrawRotationGizmo(OPERATION op, int type) + { + if (!Intersects(op, ROTATE)) + { + return; + } + ImDrawList* drawList = gContext.mDrawList; + + bool isMultipleAxesMasked = gContext.mAxisMask & (gContext.mAxisMask - 1); + bool isNoAxesMasked = !gContext.mAxisMask; + + // colors + ImU32 colors[7]; + ComputeColors(colors, type, ROTATE); + + vec_t cameraToModelNormalized; + if (gContext.mIsOrthographic) + { + matrix_t viewInverse; + viewInverse.Inverse(*(matrix_t*)&gContext.mViewMat); + cameraToModelNormalized = -viewInverse.v.dir; + } + else + { + cameraToModelNormalized = Normalized(gContext.mModel.v.position - gContext.mCameraEye); + } + + cameraToModelNormalized.TransformVector(gContext.mModelInverse); + + gContext.mRadiusSquareCenter = screenRotateSize * gContext.mHeight; + + bool hasRSC = Intersects(op, ROTATE_SCREEN); + for (int axis = 0; axis < 3; axis++) + { + if (!Intersects(op, static_cast(ROTATE_Z >> axis))) { - colors[i + 1] = (type == (int)(MT_SCALE_X + i)) ? selectionColor : GetColorU32(DIRECTION_X + i); - } - break; - // note: this internal function is only called with three possible values for operation - default: - break; - } - } - else - { - ImU32 inactiveColor = GetColorU32(INACTIVE); - for (int i = 0; i < 7; i++) - { - colors[i] = inactiveColor; - } - } - } - - static void ComputeTripodAxisAndVisibility(const int axisIndex, vec_t& dirAxis, vec_t& dirPlaneX, vec_t& dirPlaneY, bool& belowAxisLimit, bool& belowPlaneLimit, const bool localCoordinates = false) - { - dirAxis = directionUnary[axisIndex]; - dirPlaneX = directionUnary[(axisIndex + 1) % 3]; - dirPlaneY = directionUnary[(axisIndex + 2) % 3]; - - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) - { - // when using, use stored factors so the gizmo doesn't flip when we translate - - // Apply axis mask to axes and planes - belowAxisLimit = gContext.mBelowAxisLimit[axisIndex] && ((1< FLT_EPSILON) ? -1.f : 1.f; - float mulAxisX = (allowFlip && lenDirPlaneX < lenDirMinusPlaneX&& fabsf(lenDirPlaneX - lenDirMinusPlaneX) > FLT_EPSILON) ? -1.f : 1.f; - float mulAxisY = (allowFlip && lenDirPlaneY < lenDirMinusPlaneY&& fabsf(lenDirPlaneY - lenDirMinusPlaneY) > FLT_EPSILON) ? -1.f : 1.f; - dirAxis *= mulAxis; - dirPlaneX *= mulAxisX; - dirPlaneY *= mulAxisY; - - // for axis - float axisLengthInClipSpace = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirAxis * gContext.mScreenFactor, localCoordinates); - - float paraSurf = GetParallelogram(makeVect(0.f, 0.f, 0.f), dirPlaneX * gContext.mScreenFactor, dirPlaneY * gContext.mScreenFactor); - // Apply axis mask to axes and planes - belowPlaneLimit = (paraSurf > gContext.mAxisLimit) && (((1< gContext.mPlaneLimit) && !((1< (1.f - snapTension)) - { - *value = *value - modulo + snap * ((*value < 0.f) ? -1.f : 1.f); - } - } - static void ComputeSnap(vec_t& value, const float* snap) - { - for (int i = 0; i < 3; i++) - { - ComputeSnap(&value[i], snap[i]); - } - } - - static float ComputeAngleOnPlan() - { - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); - vec_t localPos = Normalized(gContext.mRayOrigin + gContext.mRayVector * len - gContext.mModel.v.position); - - vec_t perpendicularVector; - perpendicularVector.Cross(gContext.mRotationVectorSource, gContext.mTranslationPlan); - perpendicularVector.Normalize(); - float acosAngle = Clamp(Dot(localPos, gContext.mRotationVectorSource), -1.f, 1.f); - float angle = acosf(acosAngle); - angle *= (Dot(localPos, perpendicularVector) < 0.f) ? 1.f : -1.f; - return angle; - } - - static void DrawRotationGizmo(OPERATION op, int type) - { - if(!Intersects(op, ROTATE)) - { - return; - } - ImDrawList* drawList = gContext.mDrawList; - - bool isMultipleAxesMasked = gContext.mAxisMask & (gContext.mAxisMask - 1); - bool isNoAxesMasked = !gContext.mAxisMask; - - // colors - ImU32 colors[7]; - ComputeColors(colors, type, ROTATE); - - vec_t cameraToModelNormalized; - if (gContext.mIsOrthographic) - { - matrix_t viewInverse; - viewInverse.Inverse(*(matrix_t*)&gContext.mViewMat); - cameraToModelNormalized = -viewInverse.v.dir; - } - else - { - cameraToModelNormalized = Normalized(gContext.mModel.v.position - gContext.mCameraEye); - } - - cameraToModelNormalized.TransformVector(gContext.mModelInverse); - - gContext.mRadiusSquareCenter = screenRotateSize * gContext.mHeight; - - bool hasRSC = Intersects(op, ROTATE_SCREEN); - for (int axis = 0; axis < 3; axis++) - { - if(!Intersects(op, static_cast(ROTATE_Z >> axis))) - { - continue; - } - - bool isAxisMasked = (1 << (2 - axis)) & gContext.mAxisMask; - - if ((!isAxisMasked || isMultipleAxesMasked) && !isNoAxesMasked) - { - continue; - } - const bool usingAxis = (gContext.mbUsing && type == MT_ROTATE_Z - axis); - const int circleMul = (hasRSC && !usingAxis) ? 1 : 2; - - ImVec2* circlePos = (ImVec2*)alloca(sizeof(ImVec2) * (circleMul * halfCircleSegmentCount + 1)); - - float angleStart = atan2f(cameraToModelNormalized[(4 - axis) % 3], cameraToModelNormalized[(3 - axis) % 3]) + ZPI * 0.5f; - - for (int i = 0; i < circleMul * halfCircleSegmentCount + 1; i++) - { - float ng = angleStart + (float)circleMul * ZPI * ((float)i / (float)(circleMul * halfCircleSegmentCount)); - vec_t axisPos = makeVect(cosf(ng), sinf(ng), 0.f); - vec_t pos = makeVect(axisPos[axis], axisPos[(axis + 1) % 3], axisPos[(axis + 2) % 3]) * gContext.mScreenFactor * rotationDisplayFactor; - circlePos[i] = worldToPos(pos, gContext.mMVP); - } - if (!gContext.mbUsing || usingAxis) - { - drawList->AddPolyline(circlePos, circleMul* halfCircleSegmentCount + 1, colors[3 - axis], false, gContext.mStyle.RotationLineThickness); - } - - float radiusAxis = sqrtf((ImLengthSqr(worldToPos(gContext.mModel.v.position, gContext.mViewProjection) - circlePos[0]))); - if (radiusAxis > gContext.mRadiusSquareCenter) - { - gContext.mRadiusSquareCenter = radiusAxis; - } - } - if(hasRSC && (!gContext.mbUsing || type == MT_ROTATE_SCREEN) && (!isMultipleAxesMasked && isNoAxesMasked)) - { - drawList->AddCircle(worldToPos(gContext.mModel.v.position, gContext.mViewProjection), gContext.mRadiusSquareCenter, colors[0], 64, gContext.mStyle.RotationOuterLineThickness); - } - - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsRotateType(type)) - { - ImVec2 circlePos[halfCircleSegmentCount + 1]; - - circlePos[0] = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); - for (unsigned int i = 1; i < halfCircleSegmentCount + 1; i++) - { - float ng = gContext.mRotationAngle * ((float)(i - 1) / (float)(halfCircleSegmentCount - 1)); - matrix_t rotateVectorMatrix; - rotateVectorMatrix.RotationAxis(gContext.mTranslationPlan, ng); - vec_t pos; - pos.TransformPoint(gContext.mRotationVectorSource, rotateVectorMatrix); - pos *= gContext.mScreenFactor * rotationDisplayFactor; - circlePos[i] = worldToPos(pos + gContext.mModel.v.position, gContext.mViewProjection); - } - drawList->AddConvexPolyFilled(circlePos, halfCircleSegmentCount + 1, GetColorU32(ROTATION_USING_FILL)); - drawList->AddPolyline(circlePos, halfCircleSegmentCount + 1, GetColorU32(ROTATION_USING_BORDER), true, gContext.mStyle.RotationLineThickness); - - ImVec2 destinationPosOnScreen = circlePos[1]; - char tmps[512]; - ImFormatString(tmps, sizeof(tmps), rotationInfoMask[type - MT_ROTATE_X], (gContext.mRotationAngle / ZPI) * 180.f, gContext.mRotationAngle); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); - } - } - - static void DrawHatchedAxis(const vec_t& axis) - { - if (gContext.mStyle.HatchedAxisLineThickness <= 0.0f) - { - return; - } - - for (int j = 1; j < 10; j++) - { - ImVec2 baseSSpace2 = worldToPos(axis * 0.05f * (float)(j * 2) * gContext.mScreenFactor, gContext.mMVP); - ImVec2 worldDirSSpace2 = worldToPos(axis * 0.05f * (float)(j * 2 + 1) * gContext.mScreenFactor, gContext.mMVP); - gContext.mDrawList->AddLine(baseSSpace2, worldDirSSpace2, GetColorU32(HATCHED_AXIS_LINES), gContext.mStyle.HatchedAxisLineThickness); - } - } - - static void DrawScaleGizmo(OPERATION op, int type) - { - ImDrawList* drawList = gContext.mDrawList; - - if(!Intersects(op, SCALE)) - { - return; - } - - // colors - ImU32 colors[7]; - ComputeColors(colors, type, SCALE); - - // draw - vec_t scaleDisplay = { 1.f, 1.f, 1.f, 1.f }; - - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) - { - scaleDisplay = gContext.mScale; - } - - for (int i = 0; i < 3; i++) - { - if(!Intersects(op, static_cast(SCALE_X << i))) - { - continue; - } - const bool usingAxis = (gContext.mbUsing && type == MT_SCALE_X + i); - if (!gContext.mbUsing || usingAxis) - { - vec_t dirPlaneX, dirPlaneY, dirAxis; - bool belowAxisLimit, belowPlaneLimit; - ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); + continue; + } - // draw axis - if (belowAxisLimit) + bool isAxisMasked = (1 << (2 - axis)) & gContext.mAxisMask; + + if ((!isAxisMasked || isMultipleAxesMasked) && !isNoAxesMasked) { - bool hasTranslateOnAxis = Contains(op, static_cast(TRANSLATE_X << i)); - float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f; - ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVP); - ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP); - ImVec2 worldDirSSpace = worldToPos((dirAxis * markerScale * scaleDisplay[i]) * gContext.mScreenFactor, gContext.mMVP); + continue; + } + const bool usingAxis = (gContext.mbUsing && type == MT_ROTATE_Z - axis); + const int circleMul = (hasRSC && !usingAxis) ? 1 : 2; - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) - { - ImU32 scaleLineColor = GetColorU32(SCALE_LINE); - drawList->AddLine(baseSSpace, worldDirSSpaceNoScale, scaleLineColor, gContext.mStyle.ScaleLineThickness); - drawList->AddCircleFilled(worldDirSSpaceNoScale, gContext.mStyle.ScaleLineCircleSize, scaleLineColor); - } + ImVec2* circlePos = (ImVec2*)alloca(sizeof(ImVec2) * (circleMul * halfCircleSegmentCount + 1)); - if (!hasTranslateOnAxis || gContext.mbUsing) - { - drawList->AddLine(baseSSpace, worldDirSSpace, colors[i + 1], gContext.mStyle.ScaleLineThickness); - } - drawList->AddCircleFilled(worldDirSSpace, gContext.mStyle.ScaleLineCircleSize, colors[i + 1]); + float angleStart = atan2f(cameraToModelNormalized[(4 - axis) % 3], cameraToModelNormalized[(3 - axis) % 3]) + ZPI * 0.5f; - if (gContext.mAxisFactor[i] < 0.f) - { - DrawHatchedAxis(dirAxis * scaleDisplay[i]); - } + for (int i = 0; i < circleMul * halfCircleSegmentCount + 1; i++) + { + float ng = angleStart + (float)circleMul * ZPI * ((float)i / (float)(circleMul * halfCircleSegmentCount)); + vec_t axisPos = makeVect(cosf(ng), sinf(ng), 0.f); + vec_t pos = makeVect(axisPos[axis], axisPos[(axis + 1) % 3], axisPos[(axis + 2) % 3]) * gContext.mScreenFactor * + rotationDisplayFactor; + circlePos[i] = worldToPos(pos, gContext.mMVP); + } + if (!gContext.mbUsing || usingAxis) + { + drawList->AddPolyline( + circlePos, circleMul * halfCircleSegmentCount + 1, colors[3 - axis], false, gContext.mStyle.RotationLineThickness); } - } - } - - // draw screen cirle - drawList->AddCircleFilled(gContext.mScreenSquareCenter, gContext.mStyle.CenterCircleSize, colors[0], 32); - - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsScaleType(type)) - { - //ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection); - ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); - /*vec_t dif(destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y); - dif.Normalize(); - dif *= 5.f; - drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor); - drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor); - drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, destinationPosOnScreen.y - dif.y), translationLineColor, 2.f); - */ - char tmps[512]; - //vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin; - int componentInfoIndex = (type - MT_SCALE_X) * 3; - ImFormatString(tmps, sizeof(tmps), scaleInfoMask[type - MT_SCALE_X], scaleDisplay[translationInfoIndex[componentInfoIndex]]); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); - } - } - - - static void DrawScaleUniveralGizmo(OPERATION op, int type) - { - ImDrawList* drawList = gContext.mDrawList; - - if (!Intersects(op, SCALEU)) - { - return; - } - - // colors - ImU32 colors[7]; - ComputeColors(colors, type, SCALEU); - - // draw - vec_t scaleDisplay = { 1.f, 1.f, 1.f, 1.f }; - - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) - { - scaleDisplay = gContext.mScale; - } - - for (int i = 0; i < 3; i++) - { - if (!Intersects(op, static_cast(SCALE_XU << i))) - { - continue; - } - const bool usingAxis = (gContext.mbUsing && type == MT_SCALE_X + i); - if (!gContext.mbUsing || usingAxis) - { - vec_t dirPlaneX, dirPlaneY, dirAxis; - bool belowAxisLimit, belowPlaneLimit; - ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); - // draw axis - if (belowAxisLimit) + float radiusAxis = sqrtf((ImLengthSqr(worldToPos(gContext.mModel.v.position, gContext.mViewProjection) - circlePos[0]))); + if (radiusAxis > gContext.mRadiusSquareCenter) + { + gContext.mRadiusSquareCenter = radiusAxis; + } + } + if (hasRSC && (!gContext.mbUsing || type == MT_ROTATE_SCREEN) && (!isMultipleAxesMasked && isNoAxesMasked)) + { + drawList->AddCircle( + worldToPos(gContext.mModel.v.position, gContext.mViewProjection), + gContext.mRadiusSquareCenter, + colors[0], + 64, + gContext.mStyle.RotationOuterLineThickness); + } + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsRotateType(type)) + { + ImVec2 circlePos[halfCircleSegmentCount + 1]; + + circlePos[0] = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + for (unsigned int i = 1; i < halfCircleSegmentCount + 1; i++) + { + float ng = gContext.mRotationAngle * ((float)(i - 1) / (float)(halfCircleSegmentCount - 1)); + matrix_t rotateVectorMatrix; + rotateVectorMatrix.RotationAxis(gContext.mTranslationPlan, ng); + vec_t pos; + pos.TransformPoint(gContext.mRotationVectorSource, rotateVectorMatrix); + pos *= gContext.mScreenFactor * rotationDisplayFactor; + circlePos[i] = worldToPos(pos + gContext.mModel.v.position, gContext.mViewProjection); + } + drawList->AddConvexPolyFilled(circlePos, halfCircleSegmentCount + 1, GetColorU32(ROTATION_USING_FILL)); + drawList->AddPolyline( + circlePos, halfCircleSegmentCount + 1, GetColorU32(ROTATION_USING_BORDER), true, gContext.mStyle.RotationLineThickness); + + ImVec2 destinationPosOnScreen = circlePos[1]; + char tmps[512]; + ImFormatString( + tmps, sizeof(tmps), rotationInfoMask[type - MT_ROTATE_X], (gContext.mRotationAngle / ZPI) * 180.f, gContext.mRotationAngle); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); + } + } + + static void DrawHatchedAxis(const vec_t& axis) + { + if (gContext.mStyle.HatchedAxisLineThickness <= 0.0f) + { + return; + } + + for (int j = 1; j < 10; j++) + { + ImVec2 baseSSpace2 = worldToPos(axis * 0.05f * (float)(j * 2) * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace2 = worldToPos(axis * 0.05f * (float)(j * 2 + 1) * gContext.mScreenFactor, gContext.mMVP); + gContext.mDrawList->AddLine( + baseSSpace2, worldDirSSpace2, GetColorU32(HATCHED_AXIS_LINES), gContext.mStyle.HatchedAxisLineThickness); + } + } + + static void DrawScaleGizmo(OPERATION op, int type) + { + ImDrawList* drawList = gContext.mDrawList; + + if (!Intersects(op, SCALE)) + { + return; + } + + // colors + ImU32 colors[7]; + ComputeColors(colors, type, SCALE); + + // draw + vec_t scaleDisplay = { 1.f, 1.f, 1.f, 1.f }; + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) + { + scaleDisplay = gContext.mScale; + } + + for (int i = 0; i < 3; i++) + { + if (!Intersects(op, static_cast(SCALE_X << i))) + { + continue; + } + const bool usingAxis = (gContext.mbUsing && type == MT_SCALE_X + i); + if (!gContext.mbUsing || usingAxis) + { + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); + + // draw axis + if (belowAxisLimit) + { + bool hasTranslateOnAxis = Contains(op, static_cast(TRANSLATE_X << i)); + float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f; + ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace = worldToPos((dirAxis * markerScale * scaleDisplay[i]) * gContext.mScreenFactor, gContext.mMVP); + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) + { + ImU32 scaleLineColor = GetColorU32(SCALE_LINE); + drawList->AddLine(baseSSpace, worldDirSSpaceNoScale, scaleLineColor, gContext.mStyle.ScaleLineThickness); + drawList->AddCircleFilled(worldDirSSpaceNoScale, gContext.mStyle.ScaleLineCircleSize, scaleLineColor); + } + + if (!hasTranslateOnAxis || gContext.mbUsing) + { + drawList->AddLine(baseSSpace, worldDirSSpace, colors[i + 1], gContext.mStyle.ScaleLineThickness); + } + drawList->AddCircleFilled(worldDirSSpace, gContext.mStyle.ScaleLineCircleSize, colors[i + 1]); + + if (gContext.mAxisFactor[i] < 0.f) + { + DrawHatchedAxis(dirAxis * scaleDisplay[i]); + } + } + } + } + + // draw screen cirle + drawList->AddCircleFilled(gContext.mScreenSquareCenter, gContext.mStyle.CenterCircleSize, colors[0], 32); + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsScaleType(type)) + { + // ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection); + ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + /*vec_t dif(destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y); + dif.Normalize(); + dif *= 5.f; + drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor); + drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor); + drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, + destinationPosOnScreen.y - dif.y), translationLineColor, 2.f); + */ + char tmps[512]; + // vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin; + int componentInfoIndex = (type - MT_SCALE_X) * 3; + ImFormatString(tmps, sizeof(tmps), scaleInfoMask[type - MT_SCALE_X], scaleDisplay[translationInfoIndex[componentInfoIndex]]); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); + } + } + + static void DrawScaleUniveralGizmo(OPERATION op, int type) + { + ImDrawList* drawList = gContext.mDrawList; + + if (!Intersects(op, SCALEU)) + { + return; + } + + // colors + ImU32 colors[7]; + ComputeColors(colors, type, SCALEU); + + // draw + vec_t scaleDisplay = { 1.f, 1.f, 1.f, 1.f }; + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) + { + scaleDisplay = gContext.mScale; + } + + for (int i = 0; i < 3; i++) + { + if (!Intersects(op, static_cast(SCALE_XU << i))) + { + continue; + } + const bool usingAxis = (gContext.mbUsing && type == MT_SCALE_X + i); + if (!gContext.mbUsing || usingAxis) { - bool hasTranslateOnAxis = Contains(op, static_cast(TRANSLATE_X << i)); - float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f; - //ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVPLocal); - //ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP); - ImVec2 worldDirSSpace = worldToPos((dirAxis * markerScale * scaleDisplay[i]) * gContext.mScreenFactor, gContext.mMVPLocal); + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); + + // draw axis + if (belowAxisLimit) + { + bool hasTranslateOnAxis = Contains(op, static_cast(TRANSLATE_X << i)); + float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f; + // ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVPLocal); + // ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace = + worldToPos((dirAxis * markerScale * scaleDisplay[i]) * gContext.mScreenFactor, gContext.mMVPLocal); #if 0 if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) @@ -1524,1622 +1710,1680 @@ namespace IMGUIZMO_NAMESPACE } */ #endif - drawList->AddCircleFilled(worldDirSSpace, 12.f, colors[i + 1]); - } - } - } - - // draw screen cirle - drawList->AddCircle(gContext.mScreenSquareCenter, 20.f, colors[0], 32, gContext.mStyle.CenterCircleSize); - - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsScaleType(type)) - { - //ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection); - ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); - /*vec_t dif(destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y); - dif.Normalize(); - dif *= 5.f; - drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor); - drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor); - drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, destinationPosOnScreen.y - dif.y), translationLineColor, 2.f); - */ - char tmps[512]; - //vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin; - int componentInfoIndex = (type - MT_SCALE_X) * 3; - ImFormatString(tmps, sizeof(tmps), scaleInfoMask[type - MT_SCALE_X], scaleDisplay[translationInfoIndex[componentInfoIndex]]); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); - } - } - - static void DrawTranslationGizmo(OPERATION op, int type) - { - ImDrawList* drawList = gContext.mDrawList; - if (!drawList) - { - return; - } - - if(!Intersects(op, TRANSLATE)) - { - return; - } - - // colors - ImU32 colors[7]; - ComputeColors(colors, type, TRANSLATE); - - const ImVec2 origin = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); - - // draw - bool belowAxisLimit = false; - bool belowPlaneLimit = false; - for (int i = 0; i < 3; ++i) - { - vec_t dirPlaneX, dirPlaneY, dirAxis; - ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit); - - if (!gContext.mbUsing || (gContext.mbUsing && type == MT_MOVE_X + i)) - { - // draw axis - if (belowAxisLimit && Intersects(op, static_cast(TRANSLATE_X << i))) - { - ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVP); - ImVec2 worldDirSSpace = worldToPos(dirAxis * gContext.mScreenFactor, gContext.mMVP); + drawList->AddCircleFilled(worldDirSSpace, 12.f, colors[i + 1]); + } + } + } - drawList->AddLine(baseSSpace, worldDirSSpace, colors[i + 1], gContext.mStyle.TranslationLineThickness); + // draw screen cirle + drawList->AddCircle(gContext.mScreenSquareCenter, 20.f, colors[0], 32, gContext.mStyle.CenterCircleSize); - // Arrow head begin - ImVec2 dir(origin - worldDirSSpace); + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsScaleType(type)) + { + // ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection); + ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + /*vec_t dif(destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y); + dif.Normalize(); + dif *= 5.f; + drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor); + drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor); + drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, + destinationPosOnScreen.y - dif.y), translationLineColor, 2.f); + */ + char tmps[512]; + // vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin; + int componentInfoIndex = (type - MT_SCALE_X) * 3; + ImFormatString(tmps, sizeof(tmps), scaleInfoMask[type - MT_SCALE_X], scaleDisplay[translationInfoIndex[componentInfoIndex]]); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); + } + } + + static void DrawTranslationGizmo(OPERATION op, int type) + { + ImDrawList* drawList = gContext.mDrawList; + if (!drawList) + { + return; + } + + if (!Intersects(op, TRANSLATE)) + { + return; + } + + // colors + ImU32 colors[7]; + ComputeColors(colors, type, TRANSLATE); + + const ImVec2 origin = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + + // draw + bool belowAxisLimit = false; + bool belowPlaneLimit = false; + for (int i = 0; i < 3; ++i) + { + vec_t dirPlaneX, dirPlaneY, dirAxis; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit); - float d = sqrtf(ImLengthSqr(dir)); - dir /= d; // Normalize - dir *= gContext.mStyle.TranslationLineArrowSize; + if (!gContext.mbUsing || (gContext.mbUsing && type == MT_MOVE_X + i)) + { + // draw axis + if (belowAxisLimit && Intersects(op, static_cast(TRANSLATE_X << i))) + { + ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace = worldToPos(dirAxis * gContext.mScreenFactor, gContext.mMVP); + + drawList->AddLine(baseSSpace, worldDirSSpace, colors[i + 1], gContext.mStyle.TranslationLineThickness); + + // Arrow head begin + ImVec2 dir(origin - worldDirSSpace); + + float d = sqrtf(ImLengthSqr(dir)); + dir /= d; // Normalize + dir *= gContext.mStyle.TranslationLineArrowSize; + + ImVec2 ortogonalDir(dir.y, -dir.x); // Perpendicular vector + ImVec2 a(worldDirSSpace + dir); + drawList->AddTriangleFilled(worldDirSSpace - dir, a + ortogonalDir, a - ortogonalDir, colors[i + 1]); + // Arrow head end + + if (gContext.mAxisFactor[i] < 0.f) + { + DrawHatchedAxis(dirAxis); + } + } + } + // draw plane + if (!gContext.mbUsing || (gContext.mbUsing && type == MT_MOVE_YZ + i)) + { + if (belowPlaneLimit && Contains(op, TRANSLATE_PLANS[i])) + { + ImVec2 screenQuadPts[4]; + for (int j = 0; j < 4; ++j) + { + vec_t cornerWorldPos = (dirPlaneX * quadUV[j * 2] + dirPlaneY * quadUV[j * 2 + 1]) * gContext.mScreenFactor; + screenQuadPts[j] = worldToPos(cornerWorldPos, gContext.mMVP); + } + drawList->AddPolyline(screenQuadPts, 4, GetColorU32(DIRECTION_X + i), true, 1.0f); + drawList->AddConvexPolyFilled(screenQuadPts, 4, colors[i + 4]); + } + } + } - ImVec2 ortogonalDir(dir.y, -dir.x); // Perpendicular vector - ImVec2 a(worldDirSSpace + dir); - drawList->AddTriangleFilled(worldDirSSpace - dir, a + ortogonalDir, a - ortogonalDir, colors[i + 1]); - // Arrow head end + drawList->AddCircleFilled(gContext.mScreenSquareCenter, gContext.mStyle.CenterCircleSize, colors[0], 32); - if (gContext.mAxisFactor[i] < 0.f) - { - DrawHatchedAxis(dirAxis); - } + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsTranslateType(type)) + { + ImU32 translationLineColor = GetColorU32(TRANSLATION_LINE); + + ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection); + ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + vec_t dif = { destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y, 0.f, 0.f }; + dif.Normalize(); + dif *= 5.f; + drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor); + drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor); + drawList->AddLine( + ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), + ImVec2(destinationPosOnScreen.x - dif.x, destinationPosOnScreen.y - dif.y), + translationLineColor, + 2.f); + + char tmps[512]; + vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin; + int componentInfoIndex = (type - MT_MOVE_X) * 3; + ImFormatString( + tmps, + sizeof(tmps), + translationInfoMask[type - MT_MOVE_X], + deltaInfo[translationInfoIndex[componentInfoIndex]], + deltaInfo[translationInfoIndex[componentInfoIndex + 1]], + deltaInfo[translationInfoIndex[componentInfoIndex + 2]]); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); + } + } + + static bool CanActivate() + { + if (ImGui::IsMouseClicked(0) && !ImGui::IsAnyItemHovered() && !ImGui::IsAnyItemActive()) + { + return true; + } + return false; + } + + static void HandleAndDrawLocalBounds(const float* bounds, matrix_t* matrix, const float* snapValues, OPERATION operation) + { + ImGuiIO& io = ImGui::GetIO(); + ImDrawList* drawList = gContext.mDrawList; + + // compute best projection axis + vec_t axesWorldDirections[3]; + vec_t bestAxisWorldDirection = { 0.0f, 0.0f, 0.0f, 0.0f }; + int axes[3]; + unsigned int numAxes = 1; + axes[0] = gContext.mBoundsBestAxis; + int bestAxis = axes[0]; + if (!gContext.mbUsingBounds) + { + numAxes = 0; + float bestDot = 0.f; + for (int i = 0; i < 3; i++) + { + vec_t dirPlaneNormalWorld; + dirPlaneNormalWorld.TransformVector(directionUnary[i], gContext.mModelSource); + dirPlaneNormalWorld.Normalize(); + + float dt = fabsf(Dot(Normalized(gContext.mCameraEye - gContext.mModelSource.v.position), dirPlaneNormalWorld)); + if (dt >= bestDot) + { + bestDot = dt; + bestAxis = i; + bestAxisWorldDirection = dirPlaneNormalWorld; + } + + if (dt >= 0.1f) + { + axes[numAxes] = i; + axesWorldDirections[numAxes] = dirPlaneNormalWorld; + ++numAxes; + } } - } - // draw plane - if (!gContext.mbUsing || (gContext.mbUsing && type == MT_MOVE_YZ + i)) - { - if (belowPlaneLimit && Contains(op, TRANSLATE_PLANS[i])) + } + + if (numAxes == 0) + { + axes[0] = bestAxis; + axesWorldDirections[0] = bestAxisWorldDirection; + numAxes = 1; + } + + else if (bestAxis != axes[0]) + { + unsigned int bestIndex = 0; + for (unsigned int i = 0; i < numAxes; i++) { - ImVec2 screenQuadPts[4]; - for (int j = 0; j < 4; ++j) - { - vec_t cornerWorldPos = (dirPlaneX * quadUV[j * 2] + dirPlaneY * quadUV[j * 2 + 1]) * gContext.mScreenFactor; - screenQuadPts[j] = worldToPos(cornerWorldPos, gContext.mMVP); - } - drawList->AddPolyline(screenQuadPts, 4, GetColorU32(DIRECTION_X + i), true, 1.0f); - drawList->AddConvexPolyFilled(screenQuadPts, 4, colors[i + 4]); - } - } - } - - drawList->AddCircleFilled(gContext.mScreenSquareCenter, gContext.mStyle.CenterCircleSize, colors[0], 32); - - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsTranslateType(type)) - { - ImU32 translationLineColor = GetColorU32(TRANSLATION_LINE); - - ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection); - ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); - vec_t dif = { destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y, 0.f, 0.f }; - dif.Normalize(); - dif *= 5.f; - drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor); - drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor); - drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, destinationPosOnScreen.y - dif.y), translationLineColor, 2.f); - - char tmps[512]; - vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin; - int componentInfoIndex = (type - MT_MOVE_X) * 3; - ImFormatString(tmps, sizeof(tmps), translationInfoMask[type - MT_MOVE_X], deltaInfo[translationInfoIndex[componentInfoIndex]], deltaInfo[translationInfoIndex[componentInfoIndex + 1]], deltaInfo[translationInfoIndex[componentInfoIndex + 2]]); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); - } - } - - static bool CanActivate() - { - if (ImGui::IsMouseClicked(0) && !ImGui::IsAnyItemHovered() && !ImGui::IsAnyItemActive()) - { - return true; - } - return false; - } - - static void HandleAndDrawLocalBounds(const float* bounds, matrix_t* matrix, const float* snapValues, OPERATION operation) - { - ImGuiIO& io = ImGui::GetIO(); - ImDrawList* drawList = gContext.mDrawList; - - // compute best projection axis - vec_t axesWorldDirections[3]; - vec_t bestAxisWorldDirection = { 0.0f, 0.0f, 0.0f, 0.0f }; - int axes[3]; - unsigned int numAxes = 1; - axes[0] = gContext.mBoundsBestAxis; - int bestAxis = axes[0]; - if (!gContext.mbUsingBounds) - { - numAxes = 0; - float bestDot = 0.f; - for (int i = 0; i < 3; i++) - { - vec_t dirPlaneNormalWorld; - dirPlaneNormalWorld.TransformVector(directionUnary[i], gContext.mModelSource); - dirPlaneNormalWorld.Normalize(); - - float dt = fabsf(Dot(Normalized(gContext.mCameraEye - gContext.mModelSource.v.position), dirPlaneNormalWorld)); - if (dt >= bestDot) - { - bestDot = dt; - bestAxis = i; - bestAxisWorldDirection = dirPlaneNormalWorld; - } - - if (dt >= 0.1f) - { - axes[numAxes] = i; - axesWorldDirections[numAxes] = dirPlaneNormalWorld; - ++numAxes; - } - } - } - - if (numAxes == 0) - { - axes[0] = bestAxis; - axesWorldDirections[0] = bestAxisWorldDirection; - numAxes = 1; - } - - else if (bestAxis != axes[0]) - { - unsigned int bestIndex = 0; - for (unsigned int i = 0; i < numAxes; i++) - { - if (axes[i] == bestAxis) - { - bestIndex = i; - break; - } - } - int tempAxis = axes[0]; - axes[0] = axes[bestIndex]; - axes[bestIndex] = tempAxis; - vec_t tempDirection = axesWorldDirections[0]; - axesWorldDirections[0] = axesWorldDirections[bestIndex]; - axesWorldDirections[bestIndex] = tempDirection; - } - - for (unsigned int axisIndex = 0; axisIndex < numAxes; ++axisIndex) - { - bestAxis = axes[axisIndex]; - bestAxisWorldDirection = axesWorldDirections[axisIndex]; - - // corners - vec_t aabb[4]; - - int secondAxis = (bestAxis + 1) % 3; - int thirdAxis = (bestAxis + 2) % 3; - - for (int i = 0; i < 4; i++) - { - aabb[i][3] = aabb[i][bestAxis] = 0.f; - aabb[i][secondAxis] = bounds[secondAxis + 3 * (i >> 1)]; - aabb[i][thirdAxis] = bounds[thirdAxis + 3 * ((i >> 1) ^ (i & 1))]; - } - - // draw bounds - unsigned int anchorAlpha = gContext.mbEnable ? IM_COL32_BLACK : IM_COL32(0, 0, 0, 0x80); - - matrix_t boundsMVP = gContext.mModelSource * gContext.mViewProjection; - for (int i = 0; i < 4; i++) - { - ImVec2 worldBound1 = worldToPos(aabb[i], boundsMVP); - ImVec2 worldBound2 = worldToPos(aabb[(i + 1) % 4], boundsMVP); - if (!IsInContextRect(worldBound1) || !IsInContextRect(worldBound2)) - { - continue; - } - float boundDistance = sqrtf(ImLengthSqr(worldBound1 - worldBound2)); - int stepCount = (int)(boundDistance / 10.f); - stepCount = min(stepCount, 1000); - for (int j = 0; j < stepCount; j++) - { - float stepLength = 1.f / (float)stepCount; - float t1 = (float)j * stepLength; - float t2 = (float)j * stepLength + stepLength * 0.5f; - ImVec2 worldBoundSS1 = ImLerp(worldBound1, worldBound2, ImVec2(t1, t1)); - ImVec2 worldBoundSS2 = ImLerp(worldBound1, worldBound2, ImVec2(t2, t2)); - //drawList->AddLine(worldBoundSS1, worldBoundSS2, IM_COL32(0, 0, 0, 0) + anchorAlpha, 3.f); - drawList->AddLine(worldBoundSS1, worldBoundSS2, IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha, 2.f); - } - vec_t midPoint = (aabb[i] + aabb[(i + 1) % 4]) * 0.5f; - ImVec2 midBound = worldToPos(midPoint, boundsMVP); - static const float AnchorBigRadius = 8.f; - static const float AnchorSmallRadius = 6.f; - bool overBigAnchor = ImLengthSqr(worldBound1 - io.MousePos) <= (AnchorBigRadius * AnchorBigRadius); - bool overSmallAnchor = ImLengthSqr(midBound - io.MousePos) <= (AnchorBigRadius * AnchorBigRadius); - - int type = MT_NONE; - vec_t gizmoHitProportion; + if (axes[i] == bestAxis) + { + bestIndex = i; + break; + } + } + int tempAxis = axes[0]; + axes[0] = axes[bestIndex]; + axes[bestIndex] = tempAxis; + vec_t tempDirection = axesWorldDirections[0]; + axesWorldDirections[0] = axesWorldDirections[bestIndex]; + axesWorldDirections[bestIndex] = tempDirection; + } + + for (unsigned int axisIndex = 0; axisIndex < numAxes; ++axisIndex) + { + bestAxis = axes[axisIndex]; + bestAxisWorldDirection = axesWorldDirections[axisIndex]; + + // corners + vec_t aabb[4]; + + int secondAxis = (bestAxis + 1) % 3; + int thirdAxis = (bestAxis + 2) % 3; + + for (int i = 0; i < 4; i++) + { + aabb[i][3] = aabb[i][bestAxis] = 0.f; + aabb[i][secondAxis] = bounds[secondAxis + 3 * (i >> 1)]; + aabb[i][thirdAxis] = bounds[thirdAxis + 3 * ((i >> 1) ^ (i & 1))]; + } + + // draw bounds + unsigned int anchorAlpha = gContext.mbEnable ? IM_COL32_BLACK : IM_COL32(0, 0, 0, 0x80); - if(Intersects(operation, TRANSLATE)) + matrix_t boundsMVP = gContext.mModelSource * gContext.mViewProjection; + for (int i = 0; i < 4; i++) { - type = GetMoveType(operation, &gizmoHitProportion); + ImVec2 worldBound1 = worldToPos(aabb[i], boundsMVP); + ImVec2 worldBound2 = worldToPos(aabb[(i + 1) % 4], boundsMVP); + if (!IsInContextRect(worldBound1) || !IsInContextRect(worldBound2)) + { + continue; + } + float boundDistance = sqrtf(ImLengthSqr(worldBound1 - worldBound2)); + int stepCount = (int)(boundDistance / 10.f); + stepCount = min(stepCount, 1000); + for (int j = 0; j < stepCount; j++) + { + float stepLength = 1.f / (float)stepCount; + float t1 = (float)j * stepLength; + float t2 = (float)j * stepLength + stepLength * 0.5f; + ImVec2 worldBoundSS1 = ImLerp(worldBound1, worldBound2, ImVec2(t1, t1)); + ImVec2 worldBoundSS2 = ImLerp(worldBound1, worldBound2, ImVec2(t2, t2)); + // drawList->AddLine(worldBoundSS1, worldBoundSS2, IM_COL32(0, 0, 0, 0) + anchorAlpha, 3.f); + drawList->AddLine(worldBoundSS1, worldBoundSS2, IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha, 2.f); + } + vec_t midPoint = (aabb[i] + aabb[(i + 1) % 4]) * 0.5f; + ImVec2 midBound = worldToPos(midPoint, boundsMVP); + static const float AnchorBigRadius = 8.f; + static const float AnchorSmallRadius = 6.f; + bool overBigAnchor = ImLengthSqr(worldBound1 - io.MousePos) <= (AnchorBigRadius * AnchorBigRadius); + bool overSmallAnchor = ImLengthSqr(midBound - io.MousePos) <= (AnchorBigRadius * AnchorBigRadius); + + int type = MT_NONE; + vec_t gizmoHitProportion; + + if (Intersects(operation, TRANSLATE)) + { + type = GetMoveType(operation, &gizmoHitProportion); + } + if (Intersects(operation, ROTATE) && type == MT_NONE) + { + type = GetRotateType(operation); + } + if (Intersects(operation, SCALE) && type == MT_NONE) + { + type = GetScaleType(operation); + } + + if (type != MT_NONE) + { + overBigAnchor = false; + overSmallAnchor = false; + } + + ImU32 selectionColor = GetColorU32(SELECTION); + + unsigned int bigAnchorColor = overBigAnchor ? selectionColor : (IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha); + unsigned int smallAnchorColor = overSmallAnchor ? selectionColor : (IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha); + + drawList->AddCircleFilled(worldBound1, AnchorBigRadius, IM_COL32_BLACK); + drawList->AddCircleFilled(worldBound1, AnchorBigRadius - 1.2f, bigAnchorColor); + + drawList->AddCircleFilled(midBound, AnchorSmallRadius, IM_COL32_BLACK); + drawList->AddCircleFilled(midBound, AnchorSmallRadius - 1.2f, smallAnchorColor); + int oppositeIndex = (i + 2) % 4; + // big anchor on corners + if (!gContext.mbUsingBounds && gContext.mbEnable && overBigAnchor && CanActivate()) + { + gContext.mBoundsPivot.TransformPoint(aabb[(i + 2) % 4], gContext.mModelSource); + gContext.mBoundsAnchor.TransformPoint(aabb[i], gContext.mModelSource); + gContext.mBoundsPlan = BuildPlan(gContext.mBoundsAnchor, bestAxisWorldDirection); + gContext.mBoundsBestAxis = bestAxis; + gContext.mBoundsAxis[0] = secondAxis; + gContext.mBoundsAxis[1] = thirdAxis; + + gContext.mBoundsLocalPivot.Set(0.f); + gContext.mBoundsLocalPivot[secondAxis] = aabb[oppositeIndex][secondAxis]; + gContext.mBoundsLocalPivot[thirdAxis] = aabb[oppositeIndex][thirdAxis]; + + gContext.mbUsingBounds = true; + gContext.mEditingID = gContext.GetCurrentID(); + gContext.mBoundsMatrix = gContext.mModelSource; + } + // small anchor on middle of segment + if (!gContext.mbUsingBounds && gContext.mbEnable && overSmallAnchor && CanActivate()) + { + vec_t midPointOpposite = (aabb[(i + 2) % 4] + aabb[(i + 3) % 4]) * 0.5f; + gContext.mBoundsPivot.TransformPoint(midPointOpposite, gContext.mModelSource); + gContext.mBoundsAnchor.TransformPoint(midPoint, gContext.mModelSource); + gContext.mBoundsPlan = BuildPlan(gContext.mBoundsAnchor, bestAxisWorldDirection); + gContext.mBoundsBestAxis = bestAxis; + int indices[] = { secondAxis, thirdAxis }; + gContext.mBoundsAxis[0] = indices[i % 2]; + gContext.mBoundsAxis[1] = -1; + + gContext.mBoundsLocalPivot.Set(0.f); + gContext.mBoundsLocalPivot[gContext.mBoundsAxis[0]] = + aabb[oppositeIndex][indices[i % 2]]; // bounds[gContext.mBoundsAxis[0]] * (((i + 1) & 2) ? 1.f : -1.f); + + gContext.mbUsingBounds = true; + gContext.mEditingID = gContext.GetCurrentID(); + gContext.mBoundsMatrix = gContext.mModelSource; + } } - if(Intersects(operation, ROTATE) && type == MT_NONE) + + if (gContext.mbUsingBounds && (gContext.GetCurrentID() == gContext.mEditingID)) { - type = GetRotateType(operation); + matrix_t scale; + scale.SetToIdentity(); + + // compute projected mouse position on plan + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mBoundsPlan); + vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; + + // compute a reference and delta vectors base on mouse move + vec_t deltaVector = (newPos - gContext.mBoundsPivot).Abs(); + vec_t referenceVector = (gContext.mBoundsAnchor - gContext.mBoundsPivot).Abs(); + + // for 1 or 2 axes, compute a ratio that's used for scale and snap it based on resulting length + for (int i = 0; i < 2; i++) + { + int axisIndex1 = gContext.mBoundsAxis[i]; + if (axisIndex1 == -1) + { + continue; + } + + float ratioAxis = 1.f; + vec_t axisDir = gContext.mBoundsMatrix.component[axisIndex1].Abs(); + + float dtAxis = axisDir.Dot(referenceVector); + float boundSize = bounds[axisIndex1 + 3] - bounds[axisIndex1]; + if (dtAxis > FLT_EPSILON) + { + ratioAxis = axisDir.Dot(deltaVector) / dtAxis; + } + + if (snapValues) + { + float length = boundSize * ratioAxis; + ComputeSnap(&length, snapValues[axisIndex1]); + if (boundSize > FLT_EPSILON) + { + ratioAxis = length / boundSize; + } + } + scale.component[axisIndex1] *= ratioAxis; + } + + // transform matrix + matrix_t preScale, postScale; + preScale.Translation(-gContext.mBoundsLocalPivot); + postScale.Translation(gContext.mBoundsLocalPivot); + matrix_t res = preScale * scale * postScale * gContext.mBoundsMatrix; + *matrix = res; + + // info text + char tmps[512]; + ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + ImFormatString( + tmps, + sizeof(tmps), + "X: %.2f Y: %.2f Z: %.2f", + (bounds[3] - bounds[0]) * gContext.mBoundsMatrix.component[0].Length() * scale.component[0].Length(), + (bounds[4] - bounds[1]) * gContext.mBoundsMatrix.component[1].Length() * scale.component[1].Length(), + (bounds[5] - bounds[2]) * gContext.mBoundsMatrix.component[2].Length() * scale.component[2].Length()); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); } - if(Intersects(operation, SCALE) && type == MT_NONE) + + if (!io.MouseDown[0]) { - type = GetScaleType(operation); + gContext.mbUsingBounds = false; + gContext.mEditingID = -1; } + if (gContext.mbUsingBounds) + { + break; + } + } + } - if (type != MT_NONE) + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + + static int GetScaleType(OPERATION op) + { + if (gContext.mbUsing) + { + return MT_NONE; + } + ImGuiIO& io = ImGui::GetIO(); + int type = MT_NONE; + + // screen + if (io.MousePos.x >= gContext.mScreenSquareMin.x && io.MousePos.x <= gContext.mScreenSquareMax.x && + io.MousePos.y >= gContext.mScreenSquareMin.y && io.MousePos.y <= gContext.mScreenSquareMax.y && Contains(op, SCALE)) + { + type = MT_SCALE_XYZ; + } + + // compute + for (int i = 0; i < 3 && type == MT_NONE; i++) + { + if (!Intersects(op, static_cast(SCALE_X << i))) { - overBigAnchor = false; - overSmallAnchor = false; + continue; } + bool isAxisMasked = (1 << i) & gContext.mAxisMask; - ImU32 selectionColor = GetColorU32(SELECTION); + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); + dirAxis.TransformVector(gContext.mModelLocal); + dirPlaneX.TransformVector(gContext.mModelLocal); + dirPlaneY.TransformVector(gContext.mModelLocal); - unsigned int bigAnchorColor = overBigAnchor ? selectionColor : (IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha); - unsigned int smallAnchorColor = overSmallAnchor ? selectionColor : (IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha); - - drawList->AddCircleFilled(worldBound1, AnchorBigRadius, IM_COL32_BLACK); - drawList->AddCircleFilled(worldBound1, AnchorBigRadius - 1.2f, bigAnchorColor); - - drawList->AddCircleFilled(midBound, AnchorSmallRadius, IM_COL32_BLACK); - drawList->AddCircleFilled(midBound, AnchorSmallRadius - 1.2f, smallAnchorColor); - int oppositeIndex = (i + 2) % 4; - // big anchor on corners - if (!gContext.mbUsingBounds && gContext.mbEnable && overBigAnchor && CanActivate()) - { - gContext.mBoundsPivot.TransformPoint(aabb[(i + 2) % 4], gContext.mModelSource); - gContext.mBoundsAnchor.TransformPoint(aabb[i], gContext.mModelSource); - gContext.mBoundsPlan = BuildPlan(gContext.mBoundsAnchor, bestAxisWorldDirection); - gContext.mBoundsBestAxis = bestAxis; - gContext.mBoundsAxis[0] = secondAxis; - gContext.mBoundsAxis[1] = thirdAxis; - - gContext.mBoundsLocalPivot.Set(0.f); - gContext.mBoundsLocalPivot[secondAxis] = aabb[oppositeIndex][secondAxis]; - gContext.mBoundsLocalPivot[thirdAxis] = aabb[oppositeIndex][thirdAxis]; - - gContext.mbUsingBounds = true; - gContext.mEditingID = gContext.GetCurrentID(); - gContext.mBoundsMatrix = gContext.mModelSource; - } - // small anchor on middle of segment - if (!gContext.mbUsingBounds && gContext.mbEnable && overSmallAnchor && CanActivate()) - { - vec_t midPointOpposite = (aabb[(i + 2) % 4] + aabb[(i + 3) % 4]) * 0.5f; - gContext.mBoundsPivot.TransformPoint(midPointOpposite, gContext.mModelSource); - gContext.mBoundsAnchor.TransformPoint(midPoint, gContext.mModelSource); - gContext.mBoundsPlan = BuildPlan(gContext.mBoundsAnchor, bestAxisWorldDirection); - gContext.mBoundsBestAxis = bestAxis; - int indices[] = { secondAxis , thirdAxis }; - gContext.mBoundsAxis[0] = indices[i % 2]; - gContext.mBoundsAxis[1] = -1; - - gContext.mBoundsLocalPivot.Set(0.f); - gContext.mBoundsLocalPivot[gContext.mBoundsAxis[0]] = aabb[oppositeIndex][indices[i % 2]];// bounds[gContext.mBoundsAxis[0]] * (((i + 1) & 2) ? 1.f : -1.f); - - gContext.mbUsingBounds = true; - gContext.mEditingID = gContext.GetCurrentID(); - gContext.mBoundsMatrix = gContext.mModelSource; - } - } - - if (gContext.mbUsingBounds && (gContext.GetCurrentID() == gContext.mEditingID)) - { - matrix_t scale; - scale.SetToIdentity(); - - // compute projected mouse position on plan - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mBoundsPlan); - vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; + const float len = + IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, BuildPlan(gContext.mModelLocal.v.position, dirAxis)); + vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len; - // compute a reference and delta vectors base on mouse move - vec_t deltaVector = (newPos - gContext.mBoundsPivot).Abs(); - vec_t referenceVector = (gContext.mBoundsAnchor - gContext.mBoundsPivot).Abs(); + const float startOffset = Contains(op, static_cast(TRANSLATE_X << i)) ? 1.0f : 0.1f; + const float endOffset = Contains(op, static_cast(TRANSLATE_X << i)) ? 1.4f : 1.0f; + const ImVec2 posOnPlanScreen = worldToPos(posOnPlan, gContext.mViewProjection); + const ImVec2 axisStartOnScreen = + worldToPos(gContext.mModelLocal.v.position + dirAxis * gContext.mScreenFactor * startOffset, gContext.mViewProjection); + const ImVec2 axisEndOnScreen = + worldToPos(gContext.mModelLocal.v.position + dirAxis * gContext.mScreenFactor * endOffset, gContext.mViewProjection); - // for 1 or 2 axes, compute a ratio that's used for scale and snap it based on resulting length - for (int i = 0; i < 2; i++) + vec_t closestPointOnAxis = PointOnSegment(makeVect(posOnPlanScreen), makeVect(axisStartOnScreen), makeVect(axisEndOnScreen)); + + if ((closestPointOnAxis - makeVect(posOnPlanScreen)).Length() < 12.f) // pixel size { - int axisIndex1 = gContext.mBoundsAxis[i]; - if (axisIndex1 == -1) - { - continue; - } + if (!isAxisMasked) + type = MT_SCALE_X + i; + } + } - float ratioAxis = 1.f; - vec_t axisDir = gContext.mBoundsMatrix.component[axisIndex1].Abs(); + // universal - float dtAxis = axisDir.Dot(referenceVector); - float boundSize = bounds[axisIndex1 + 3] - bounds[axisIndex1]; - if (dtAxis > FLT_EPSILON) - { - ratioAxis = axisDir.Dot(deltaVector) / dtAxis; - } + vec_t deltaScreen = { io.MousePos.x - gContext.mScreenSquareCenter.x, io.MousePos.y - gContext.mScreenSquareCenter.y, 0.f, 0.f }; + float dist = deltaScreen.Length(); + if (Contains(op, SCALEU) && dist >= 17.0f && dist < 23.0f) + { + type = MT_SCALE_XYZ; + } - if (snapValues) - { - float length = boundSize * ratioAxis; - ComputeSnap(&length, snapValues[axisIndex1]); - if (boundSize > FLT_EPSILON) - { - ratioAxis = length / boundSize; - } - } - scale.component[axisIndex1] *= ratioAxis; + for (int i = 0; i < 3 && type == MT_NONE; i++) + { + if (!Intersects(op, static_cast(SCALE_XU << i))) + { + continue; } - // transform matrix - matrix_t preScale, postScale; - preScale.Translation(-gContext.mBoundsLocalPivot); - postScale.Translation(gContext.mBoundsLocalPivot); - matrix_t res = preScale * scale * postScale * gContext.mBoundsMatrix; - *matrix = res; - - // info text - char tmps[512]; - ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); - ImFormatString(tmps, sizeof(tmps), "X: %.2f Y: %.2f Z: %.2f" - , (bounds[3] - bounds[0]) * gContext.mBoundsMatrix.component[0].Length() * scale.component[0].Length() - , (bounds[4] - bounds[1]) * gContext.mBoundsMatrix.component[1].Length() * scale.component[1].Length() - , (bounds[5] - bounds[2]) * gContext.mBoundsMatrix.component[2].Length() * scale.component[2].Length() - ); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); - } + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); - if (!io.MouseDown[0]) { - gContext.mbUsingBounds = false; - gContext.mEditingID = -1; - } - if (gContext.mbUsingBounds) - { - break; - } - } - } - - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // - - static int GetScaleType(OPERATION op) - { - if (gContext.mbUsing) - { - return MT_NONE; - } - ImGuiIO& io = ImGui::GetIO(); - int type = MT_NONE; - - // screen - if (io.MousePos.x >= gContext.mScreenSquareMin.x && io.MousePos.x <= gContext.mScreenSquareMax.x && - io.MousePos.y >= gContext.mScreenSquareMin.y && io.MousePos.y <= gContext.mScreenSquareMax.y && - Contains(op, SCALE)) - { - type = MT_SCALE_XYZ; - } - - // compute - for (int i = 0; i < 3 && type == MT_NONE; i++) - { - if(!Intersects(op, static_cast(SCALE_X << i))) - { - continue; - } - bool isAxisMasked = (1 << i) & gContext.mAxisMask; - - vec_t dirPlaneX, dirPlaneY, dirAxis; - bool belowAxisLimit, belowPlaneLimit; - ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); - dirAxis.TransformVector(gContext.mModelLocal); - dirPlaneX.TransformVector(gContext.mModelLocal); - dirPlaneY.TransformVector(gContext.mModelLocal); - - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, BuildPlan(gContext.mModelLocal.v.position, dirAxis)); - vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len; - - const float startOffset = Contains(op, static_cast(TRANSLATE_X << i)) ? 1.0f : 0.1f; - const float endOffset = Contains(op, static_cast(TRANSLATE_X << i)) ? 1.4f : 1.0f; - const ImVec2 posOnPlanScreen = worldToPos(posOnPlan, gContext.mViewProjection); - const ImVec2 axisStartOnScreen = worldToPos(gContext.mModelLocal.v.position + dirAxis * gContext.mScreenFactor * startOffset, gContext.mViewProjection); - const ImVec2 axisEndOnScreen = worldToPos(gContext.mModelLocal.v.position + dirAxis * gContext.mScreenFactor * endOffset, gContext.mViewProjection); - - vec_t closestPointOnAxis = PointOnSegment(makeVect(posOnPlanScreen), makeVect(axisStartOnScreen), makeVect(axisEndOnScreen)); - - if ((closestPointOnAxis - makeVect(posOnPlanScreen)).Length() < 12.f) // pixel size - { - if (!isAxisMasked) - type = MT_SCALE_X + i; - } - } - - // universal - - vec_t deltaScreen = { io.MousePos.x - gContext.mScreenSquareCenter.x, io.MousePos.y - gContext.mScreenSquareCenter.y, 0.f, 0.f }; - float dist = deltaScreen.Length(); - if (Contains(op, SCALEU) && dist >= 17.0f && dist < 23.0f) - { - type = MT_SCALE_XYZ; - } - - for (int i = 0; i < 3 && type == MT_NONE; i++) - { - if (!Intersects(op, static_cast(SCALE_XU << i))) - { - continue; - } - - vec_t dirPlaneX, dirPlaneY, dirAxis; - bool belowAxisLimit, belowPlaneLimit; - ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); - - // draw axis - if (belowAxisLimit) - { - bool hasTranslateOnAxis = Contains(op, static_cast(TRANSLATE_X << i)); - float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f; - //ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVPLocal); - //ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP); - ImVec2 worldDirSSpace = worldToPos((dirAxis * markerScale) * gContext.mScreenFactor, gContext.mMVPLocal); - - float distance = sqrtf(ImLengthSqr(worldDirSSpace - io.MousePos)); - if (distance < 12.f) - { - type = MT_SCALE_X + i; - } - } - } - return type; - } - - static int GetRotateType(OPERATION op) - { - if (gContext.mbUsing) - { - return MT_NONE; - } - - bool isNoAxesMasked = !gContext.mAxisMask; - bool isMultipleAxesMasked = gContext.mAxisMask & (gContext.mAxisMask - 1); - - ImGuiIO& io = ImGui::GetIO(); - int type = MT_NONE; - - vec_t deltaScreen = { io.MousePos.x - gContext.mScreenSquareCenter.x, io.MousePos.y - gContext.mScreenSquareCenter.y, 0.f, 0.f }; - float dist = deltaScreen.Length(); - if (Intersects(op, ROTATE_SCREEN) && dist >= (gContext.mRadiusSquareCenter - 4.0f) && dist < (gContext.mRadiusSquareCenter + 4.0f)) - { - if (!isNoAxesMasked) + // draw axis + if (belowAxisLimit) + { + bool hasTranslateOnAxis = Contains(op, static_cast(TRANSLATE_X << i)); + float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f; + // ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVPLocal); + // ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace = worldToPos((dirAxis * markerScale) * gContext.mScreenFactor, gContext.mMVPLocal); + + float distance = sqrtf(ImLengthSqr(worldDirSSpace - io.MousePos)); + if (distance < 12.f) + { + type = MT_SCALE_X + i; + } + } + } + return type; + } + + static int GetRotateType(OPERATION op) + { + if (gContext.mbUsing) + { return MT_NONE; - type = MT_ROTATE_SCREEN; - } - - const vec_t planNormals[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir }; - - vec_t modelViewPos; - modelViewPos.TransformPoint(gContext.mModel.v.position, gContext.mViewMat); - - for (int i = 0; i < 3 && type == MT_NONE; i++) - { - if(!Intersects(op, static_cast(ROTATE_X << i))) - { - continue; - } - bool isAxisMasked = (1 << i) & gContext.mAxisMask; - // pickup plan - vec_t pickupPlan = BuildPlan(gContext.mModel.v.position, planNormals[i]); - - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, pickupPlan); - const vec_t intersectWorldPos = gContext.mRayOrigin + gContext.mRayVector * len; - vec_t intersectViewPos; - intersectViewPos.TransformPoint(intersectWorldPos, gContext.mViewMat); - - if (ImAbs(modelViewPos.z) - ImAbs(intersectViewPos.z) < -FLT_EPSILON) - { - continue; - } - - const vec_t localPos = intersectWorldPos - gContext.mModel.v.position; - vec_t idealPosOnCircle = Normalized(localPos); - idealPosOnCircle.TransformVector(gContext.mModelInverse); - const ImVec2 idealPosOnCircleScreen = worldToPos(idealPosOnCircle * rotationDisplayFactor * gContext.mScreenFactor, gContext.mMVP); - - //gContext.mDrawList->AddCircle(idealPosOnCircleScreen, 5.f, IM_COL32_WHITE); - const ImVec2 distanceOnScreen = idealPosOnCircleScreen - io.MousePos; - - const float distance = makeVect(distanceOnScreen).Length(); - if (distance < 8.f) // pixel size - { - if ((!isAxisMasked || isMultipleAxesMasked) && !isNoAxesMasked) - break; - type = MT_ROTATE_X + i; - } - } - - return type; - } - - static int GetMoveType(OPERATION op, vec_t* gizmoHitProportion) - { - if(!Intersects(op, TRANSLATE) || gContext.mbUsing || !gContext.mbMouseOver) - { - return MT_NONE; - } - - bool isNoAxesMasked = !gContext.mAxisMask; - bool isMultipleAxesMasked = gContext.mAxisMask & (gContext.mAxisMask - 1); - - ImGuiIO& io = ImGui::GetIO(); - int type = MT_NONE; - - // screen - if (io.MousePos.x >= gContext.mScreenSquareMin.x && io.MousePos.x <= gContext.mScreenSquareMax.x && - io.MousePos.y >= gContext.mScreenSquareMin.y && io.MousePos.y <= gContext.mScreenSquareMax.y && - Contains(op, TRANSLATE)) - { - type = MT_MOVE_SCREEN; - } - - const vec_t screenCoord = makeVect(io.MousePos - ImVec2(gContext.mX, gContext.mY)); - - // compute - for (int i = 0; i < 3 && type == MT_NONE; i++) - { - bool isAxisMasked = (1 << i) & gContext.mAxisMask; - vec_t dirPlaneX, dirPlaneY, dirAxis; - bool belowAxisLimit, belowPlaneLimit; - ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit); - dirAxis.TransformVector(gContext.mModel); - dirPlaneX.TransformVector(gContext.mModel); - dirPlaneY.TransformVector(gContext.mModel); - - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, BuildPlan(gContext.mModel.v.position, dirAxis)); - vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len; - - const ImVec2 axisStartOnScreen = worldToPos(gContext.mModel.v.position + dirAxis * gContext.mScreenFactor * 0.1f, gContext.mViewProjection) - ImVec2(gContext.mX, gContext.mY); - const ImVec2 axisEndOnScreen = worldToPos(gContext.mModel.v.position + dirAxis * gContext.mScreenFactor, gContext.mViewProjection) - ImVec2(gContext.mX, gContext.mY); - - vec_t closestPointOnAxis = PointOnSegment(screenCoord, makeVect(axisStartOnScreen), makeVect(axisEndOnScreen)); - if ((closestPointOnAxis - screenCoord).Length() < 12.f && Intersects(op, static_cast(TRANSLATE_X << i))) // pixel size - { - if (isAxisMasked) - break; - type = MT_MOVE_X + i; - } - - const float dx = dirPlaneX.Dot3((posOnPlan - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor)); - const float dy = dirPlaneY.Dot3((posOnPlan - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor)); - if (belowPlaneLimit && dx >= quadUV[0] && dx <= quadUV[4] && dy >= quadUV[1] && dy <= quadUV[3] && Contains(op, TRANSLATE_PLANS[i])) - { - if ((!isAxisMasked || isMultipleAxesMasked) && !isNoAxesMasked) - break; - type = MT_MOVE_YZ + i; - } - - if (gizmoHitProportion) - { - *gizmoHitProportion = makeVect(dx, dy, 0.f); - } - } - return type; - } - - static bool HandleTranslation(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) - { - if(!Intersects(op, TRANSLATE) || type != MT_NONE) - { - return false; - } - const ImGuiIO& io = ImGui::GetIO(); - const bool applyRotationLocaly = gContext.mMode == LOCAL || type == MT_MOVE_SCREEN; - bool modified = false; - - // move - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsTranslateType(gContext.mCurrentOperation)) - { -#if IMGUI_VERSION_NUM >= 18723 - ImGui::SetNextFrameWantCaptureMouse(true); -#else - ImGui::CaptureMouseFromApp(); -#endif - const float signedLength = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); - const float len = fabsf(signedLength); // near plan - const vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; - - // compute delta - const vec_t newOrigin = newPos - gContext.mRelativeOrigin * gContext.mScreenFactor; - vec_t delta = newOrigin - gContext.mModel.v.position; - - // 1 axis constraint - if (gContext.mCurrentOperation >= MT_MOVE_X && gContext.mCurrentOperation <= MT_MOVE_Z) - { - const int axisIndex = gContext.mCurrentOperation - MT_MOVE_X; - const vec_t& axisValue = *(vec_t*)&gContext.mModel.m[axisIndex]; - const float lengthOnAxis = Dot(axisValue, delta); - delta = axisValue * lengthOnAxis; - } - - // snap - if (snap) - { - vec_t cumulativeDelta = gContext.mModel.v.position + delta - gContext.mMatrixOrigin; - if (applyRotationLocaly) + } + + bool isNoAxesMasked = !gContext.mAxisMask; + bool isMultipleAxesMasked = gContext.mAxisMask & (gContext.mAxisMask - 1); + + ImGuiIO& io = ImGui::GetIO(); + int type = MT_NONE; + + vec_t deltaScreen = { io.MousePos.x - gContext.mScreenSquareCenter.x, io.MousePos.y - gContext.mScreenSquareCenter.y, 0.f, 0.f }; + float dist = deltaScreen.Length(); + if (Intersects(op, ROTATE_SCREEN) && dist >= (gContext.mRadiusSquareCenter - 4.0f) && dist < (gContext.mRadiusSquareCenter + 4.0f)) + { + if (!isNoAxesMasked) + return MT_NONE; + type = MT_ROTATE_SCREEN; + } + + const vec_t planNormals[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir }; + + vec_t modelViewPos; + modelViewPos.TransformPoint(gContext.mModel.v.position, gContext.mViewMat); + + for (int i = 0; i < 3 && type == MT_NONE; i++) + { + if (!Intersects(op, static_cast(ROTATE_X << i))) { - matrix_t modelSourceNormalized = gContext.mModelSource; - modelSourceNormalized.OrthoNormalize(); - matrix_t modelSourceNormalizedInverse; - modelSourceNormalizedInverse.Inverse(modelSourceNormalized); - cumulativeDelta.TransformVector(modelSourceNormalizedInverse); - ComputeSnap(cumulativeDelta, snap); - cumulativeDelta.TransformVector(modelSourceNormalized); + continue; } - else + bool isAxisMasked = (1 << i) & gContext.mAxisMask; + // pickup plan + vec_t pickupPlan = BuildPlan(gContext.mModel.v.position, planNormals[i]); + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, pickupPlan); + const vec_t intersectWorldPos = gContext.mRayOrigin + gContext.mRayVector * len; + vec_t intersectViewPos; + intersectViewPos.TransformPoint(intersectWorldPos, gContext.mViewMat); + + if (ImAbs(modelViewPos.z) - ImAbs(intersectViewPos.z) < -FLT_EPSILON) { - ComputeSnap(cumulativeDelta, snap); + continue; } - delta = gContext.mMatrixOrigin + cumulativeDelta - gContext.mModel.v.position; - } + const vec_t localPos = intersectWorldPos - gContext.mModel.v.position; + vec_t idealPosOnCircle = Normalized(localPos); + idealPosOnCircle.TransformVector(gContext.mModelInverse); + const ImVec2 idealPosOnCircleScreen = + worldToPos(idealPosOnCircle * rotationDisplayFactor * gContext.mScreenFactor, gContext.mMVP); - if (delta != gContext.mTranslationLastDelta) - { - modified = true; - } - gContext.mTranslationLastDelta = delta; + // gContext.mDrawList->AddCircle(idealPosOnCircleScreen, 5.f, IM_COL32_WHITE); + const ImVec2 distanceOnScreen = idealPosOnCircleScreen - io.MousePos; - // compute matrix & delta - matrix_t deltaMatrixTranslation; - deltaMatrixTranslation.Translation(delta); - if (deltaMatrix) - { - memcpy(deltaMatrix, deltaMatrixTranslation.m16, sizeof(float) * 16); - } + const float distance = makeVect(distanceOnScreen).Length(); + if (distance < 8.f) // pixel size + { + if ((!isAxisMasked || isMultipleAxesMasked) && !isNoAxesMasked) + break; + type = MT_ROTATE_X + i; + } + } - const matrix_t res = gContext.mModelSource * deltaMatrixTranslation; - *(matrix_t*)matrix = res; + return type; + } - if (!io.MouseDown[0]) - { - gContext.mbUsing = false; - } - - type = gContext.mCurrentOperation; - } - else - { - // find new possible way to move - vec_t gizmoHitProportion; - type = gContext.mbOverGizmoHotspot ? MT_NONE : GetMoveType(op, &gizmoHitProportion); - gContext.mbOverGizmoHotspot |= type != MT_NONE; - if (type != MT_NONE) - { + static int GetMoveType(OPERATION op, vec_t* gizmoHitProportion) + { + if (!Intersects(op, TRANSLATE) || gContext.mbUsing || !gContext.mbMouseOver) + { + return MT_NONE; + } + + bool isNoAxesMasked = !gContext.mAxisMask; + bool isMultipleAxesMasked = gContext.mAxisMask & (gContext.mAxisMask - 1); + + ImGuiIO& io = ImGui::GetIO(); + int type = MT_NONE; + + // screen + if (io.MousePos.x >= gContext.mScreenSquareMin.x && io.MousePos.x <= gContext.mScreenSquareMax.x && + io.MousePos.y >= gContext.mScreenSquareMin.y && io.MousePos.y <= gContext.mScreenSquareMax.y && Contains(op, TRANSLATE)) + { + type = MT_MOVE_SCREEN; + } + + const vec_t screenCoord = makeVect(io.MousePos - ImVec2(gContext.mX, gContext.mY)); + + // compute + for (int i = 0; i < 3 && type == MT_NONE; i++) + { + bool isAxisMasked = (1 << i) & gContext.mAxisMask; + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit); + dirAxis.TransformVector(gContext.mModel); + dirPlaneX.TransformVector(gContext.mModel); + dirPlaneY.TransformVector(gContext.mModel); + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, BuildPlan(gContext.mModel.v.position, dirAxis)); + vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len; + + const ImVec2 axisStartOnScreen = + worldToPos(gContext.mModel.v.position + dirAxis * gContext.mScreenFactor * 0.1f, gContext.mViewProjection) - + ImVec2(gContext.mX, gContext.mY); + const ImVec2 axisEndOnScreen = + worldToPos(gContext.mModel.v.position + dirAxis * gContext.mScreenFactor, gContext.mViewProjection) - + ImVec2(gContext.mX, gContext.mY); + + vec_t closestPointOnAxis = PointOnSegment(screenCoord, makeVect(axisStartOnScreen), makeVect(axisEndOnScreen)); + if ((closestPointOnAxis - screenCoord).Length() < 12.f && + Intersects(op, static_cast(TRANSLATE_X << i))) // pixel size + { + if (isAxisMasked) + break; + type = MT_MOVE_X + i; + } + + const float dx = dirPlaneX.Dot3((posOnPlan - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor)); + const float dy = dirPlaneY.Dot3((posOnPlan - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor)); + if (belowPlaneLimit && dx >= quadUV[0] && dx <= quadUV[4] && dy >= quadUV[1] && dy <= quadUV[3] && + Contains(op, TRANSLATE_PLANS[i])) + { + if ((!isAxisMasked || isMultipleAxesMasked) && !isNoAxesMasked) + break; + type = MT_MOVE_YZ + i; + } + + if (gizmoHitProportion) + { + *gizmoHitProportion = makeVect(dx, dy, 0.f); + } + } + return type; + } + + static bool HandleTranslation(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) + { + if (!Intersects(op, TRANSLATE) || type != MT_NONE) + { + return false; + } + const ImGuiIO& io = ImGui::GetIO(); + const bool applyRotationLocaly = gContext.mMode == LOCAL || type == MT_MOVE_SCREEN; + bool modified = false; + + // move + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsTranslateType(gContext.mCurrentOperation)) + { #if IMGUI_VERSION_NUM >= 18723 ImGui::SetNextFrameWantCaptureMouse(true); #else ImGui::CaptureMouseFromApp(); #endif - } - if (CanActivate() && type != MT_NONE) - { - gContext.mbUsing = true; - gContext.mEditingID = gContext.GetCurrentID(); - gContext.mCurrentOperation = type; - vec_t movePlanNormal[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, - gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, - -gContext.mCameraDir }; - - vec_t cameraToModelNormalized = Normalized(gContext.mModel.v.position - gContext.mCameraEye); - for (unsigned int i = 0; i < 3; i++) - { - vec_t orthoVector = Cross(movePlanNormal[i], cameraToModelNormalized); - movePlanNormal[i].Cross(orthoVector); - movePlanNormal[i].Normalize(); + const float signedLength = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + const float len = fabsf(signedLength); // near plan + const vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; + + // compute delta + const vec_t newOrigin = newPos - gContext.mRelativeOrigin * gContext.mScreenFactor; + vec_t delta = newOrigin - gContext.mModel.v.position; + + // 1 axis constraint + if (gContext.mCurrentOperation >= MT_MOVE_X && gContext.mCurrentOperation <= MT_MOVE_Z) + { + const int axisIndex = gContext.mCurrentOperation - MT_MOVE_X; + const vec_t& axisValue = *(vec_t*)&gContext.mModel.m[axisIndex]; + const float lengthOnAxis = Dot(axisValue, delta); + delta = axisValue * lengthOnAxis; } - // pickup plan - gContext.mTranslationPlan = BuildPlan(gContext.mModel.v.position, movePlanNormal[type - MT_MOVE_X]); - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); - gContext.mTranslationPlanOrigin = gContext.mRayOrigin + gContext.mRayVector * len; - gContext.mMatrixOrigin = gContext.mModel.v.position; - - gContext.mRelativeOrigin = (gContext.mTranslationPlanOrigin - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor); - } - } - return modified; - } - - static bool HandleScale(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) - { - if((!Intersects(op, SCALE) && !Intersects(op, SCALEU)) || type != MT_NONE || !gContext.mbMouseOver) - { - return false; - } - ImGuiIO& io = ImGui::GetIO(); - bool modified = false; - - if (!gContext.mbUsing) - { - // find new possible way to scale - type = gContext.mbOverGizmoHotspot ? MT_NONE : GetScaleType(op); - gContext.mbOverGizmoHotspot |= type != MT_NONE; - - if (type != MT_NONE) - { + + // snap + if (snap) + { + vec_t cumulativeDelta = gContext.mModel.v.position + delta - gContext.mMatrixOrigin; + if (applyRotationLocaly) + { + matrix_t modelSourceNormalized = gContext.mModelSource; + modelSourceNormalized.OrthoNormalize(); + matrix_t modelSourceNormalizedInverse; + modelSourceNormalizedInverse.Inverse(modelSourceNormalized); + cumulativeDelta.TransformVector(modelSourceNormalizedInverse); + ComputeSnap(cumulativeDelta, snap); + cumulativeDelta.TransformVector(modelSourceNormalized); + } + else + { + ComputeSnap(cumulativeDelta, snap); + } + delta = gContext.mMatrixOrigin + cumulativeDelta - gContext.mModel.v.position; + } + + if (delta != gContext.mTranslationLastDelta) + { + modified = true; + } + gContext.mTranslationLastDelta = delta; + + // compute matrix & delta + matrix_t deltaMatrixTranslation; + deltaMatrixTranslation.Translation(delta); + if (deltaMatrix) + { + memcpy(deltaMatrix, deltaMatrixTranslation.m16, sizeof(float) * 16); + } + + const matrix_t res = gContext.mModelSource * deltaMatrixTranslation; + *(matrix_t*)matrix = res; + + if (!io.MouseDown[0]) + { + gContext.mbUsing = false; + } + + type = gContext.mCurrentOperation; + } + else + { + // find new possible way to move + vec_t gizmoHitProportion; + type = gContext.mbOverGizmoHotspot ? MT_NONE : GetMoveType(op, &gizmoHitProportion); + gContext.mbOverGizmoHotspot |= type != MT_NONE; + if (type != MT_NONE) + { #if IMGUI_VERSION_NUM >= 18723 - ImGui::SetNextFrameWantCaptureMouse(true); + ImGui::SetNextFrameWantCaptureMouse(true); #else - ImGui::CaptureMouseFromApp(); + ImGui::CaptureMouseFromApp(); #endif - } - if (CanActivate() && type != MT_NONE) - { - gContext.mbUsing = true; - gContext.mEditingID = gContext.GetCurrentID(); - gContext.mCurrentOperation = type; - const vec_t movePlanNormal[] = { gContext.mModelLocal.v.up, gContext.mModelLocal.v.dir, gContext.mModelLocal.v.right, gContext.mModelLocal.v.dir, gContext.mModelLocal.v.up, gContext.mModelLocal.v.right, -gContext.mCameraDir }; - // pickup plan + } + if (CanActivate() && type != MT_NONE) + { + gContext.mbUsing = true; + gContext.mEditingID = gContext.GetCurrentID(); + gContext.mCurrentOperation = type; + vec_t movePlanNormal[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, gContext.mModel.v.right, + gContext.mModel.v.up, gContext.mModel.v.dir, -gContext.mCameraDir }; + + vec_t cameraToModelNormalized = Normalized(gContext.mModel.v.position - gContext.mCameraEye); + for (unsigned int i = 0; i < 3; i++) + { + vec_t orthoVector = Cross(movePlanNormal[i], cameraToModelNormalized); + movePlanNormal[i].Cross(orthoVector); + movePlanNormal[i].Normalize(); + } + // pickup plan + gContext.mTranslationPlan = BuildPlan(gContext.mModel.v.position, movePlanNormal[type - MT_MOVE_X]); + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + gContext.mTranslationPlanOrigin = gContext.mRayOrigin + gContext.mRayVector * len; + gContext.mMatrixOrigin = gContext.mModel.v.position; + + gContext.mRelativeOrigin = (gContext.mTranslationPlanOrigin - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor); + } + } + return modified; + } + + static bool HandleScale(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) + { + if ((!Intersects(op, SCALE) && !Intersects(op, SCALEU)) || type != MT_NONE || !gContext.mbMouseOver) + { + return false; + } + ImGuiIO& io = ImGui::GetIO(); + bool modified = false; + + if (!gContext.mbUsing) + { + // find new possible way to scale + type = gContext.mbOverGizmoHotspot ? MT_NONE : GetScaleType(op); + gContext.mbOverGizmoHotspot |= type != MT_NONE; - gContext.mTranslationPlan = BuildPlan(gContext.mModelLocal.v.position, movePlanNormal[type - MT_SCALE_X]); - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); - gContext.mTranslationPlanOrigin = gContext.mRayOrigin + gContext.mRayVector * len; - gContext.mMatrixOrigin = gContext.mModelLocal.v.position; - gContext.mScale.Set(1.f, 1.f, 1.f); - gContext.mRelativeOrigin = (gContext.mTranslationPlanOrigin - gContext.mModelLocal.v.position) * (1.f / gContext.mScreenFactor); - gContext.mScaleValueOrigin = makeVect(gContext.mModelSource.v.right.Length(), gContext.mModelSource.v.up.Length(), gContext.mModelSource.v.dir.Length()); - gContext.mSaveMousePosx = io.MousePos.x; - } - } - // scale - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsScaleType(gContext.mCurrentOperation)) - { + if (type != MT_NONE) + { #if IMGUI_VERSION_NUM >= 18723 - ImGui::SetNextFrameWantCaptureMouse(true); + ImGui::SetNextFrameWantCaptureMouse(true); #else - ImGui::CaptureMouseFromApp(); + ImGui::CaptureMouseFromApp(); #endif - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); - vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; - vec_t newOrigin = newPos - gContext.mRelativeOrigin * gContext.mScreenFactor; - vec_t delta = newOrigin - gContext.mModelLocal.v.position; - - // 1 axis constraint - if (gContext.mCurrentOperation >= MT_SCALE_X && gContext.mCurrentOperation <= MT_SCALE_Z) - { - int axisIndex = gContext.mCurrentOperation - MT_SCALE_X; - const vec_t& axisValue = *(vec_t*)&gContext.mModelLocal.m[axisIndex]; - float lengthOnAxis = Dot(axisValue, delta); - delta = axisValue * lengthOnAxis; - - vec_t baseVector = gContext.mTranslationPlanOrigin - gContext.mModelLocal.v.position; - float ratio = Dot(axisValue, baseVector + delta) / Dot(axisValue, baseVector); - - gContext.mScale[axisIndex] = max(ratio, 0.001f); - } - else - { - float scaleDelta = (io.MousePos.x - gContext.mSaveMousePosx) * 0.01f; - gContext.mScale.Set(max(1.f + scaleDelta, 0.001f)); - } - - // snap - if (snap) - { - float scaleSnap[] = { snap[0], snap[0], snap[0] }; - ComputeSnap(gContext.mScale, scaleSnap); - } - - // no 0 allowed - for (int i = 0; i < 3; i++) - gContext.mScale[i] = max(gContext.mScale[i], 0.001f); - - if (gContext.mScaleLast != gContext.mScale) - { - modified = true; - } - gContext.mScaleLast = gContext.mScale; - - // compute matrix & delta - matrix_t deltaMatrixScale; - deltaMatrixScale.Scale(gContext.mScale * gContext.mScaleValueOrigin); - - matrix_t res = deltaMatrixScale * gContext.mModelLocal; - *(matrix_t*)matrix = res; - - if (deltaMatrix) - { - vec_t deltaScale = gContext.mScale * gContext.mScaleValueOrigin; - - vec_t originalScaleDivider; - originalScaleDivider.x = 1 / gContext.mModelScaleOrigin.x; - originalScaleDivider.y = 1 / gContext.mModelScaleOrigin.y; - originalScaleDivider.z = 1 / gContext.mModelScaleOrigin.z; - - deltaScale = deltaScale * originalScaleDivider; - - deltaMatrixScale.Scale(deltaScale); - memcpy(deltaMatrix, deltaMatrixScale.m16, sizeof(float) * 16); - } - - if (!io.MouseDown[0]) - { - gContext.mbUsing = false; - gContext.mScale.Set(1.f, 1.f, 1.f); - } - - type = gContext.mCurrentOperation; - } - return modified; - } - - static bool HandleRotation(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) - { - if(!Intersects(op, ROTATE) || type != MT_NONE || !gContext.mbMouseOver) - { - return false; - } - ImGuiIO& io = ImGui::GetIO(); - bool applyRotationLocaly = gContext.mMode == LOCAL; - bool modified = false; - - if (!gContext.mbUsing) - { - type = gContext.mbOverGizmoHotspot ? MT_NONE : GetRotateType(op); - gContext.mbOverGizmoHotspot |= type != MT_NONE; - - if (type != MT_NONE) - { + } + if (CanActivate() && type != MT_NONE) + { + gContext.mbUsing = true; + gContext.mEditingID = gContext.GetCurrentID(); + gContext.mCurrentOperation = type; + const vec_t movePlanNormal[] = { gContext.mModelLocal.v.up, gContext.mModelLocal.v.dir, gContext.mModelLocal.v.right, + gContext.mModelLocal.v.dir, gContext.mModelLocal.v.up, gContext.mModelLocal.v.right, + -gContext.mCameraDir }; + // pickup plan + + gContext.mTranslationPlan = BuildPlan(gContext.mModelLocal.v.position, movePlanNormal[type - MT_SCALE_X]); + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + gContext.mTranslationPlanOrigin = gContext.mRayOrigin + gContext.mRayVector * len; + gContext.mMatrixOrigin = gContext.mModelLocal.v.position; + gContext.mScale.Set(1.f, 1.f, 1.f); + gContext.mRelativeOrigin = + (gContext.mTranslationPlanOrigin - gContext.mModelLocal.v.position) * (1.f / gContext.mScreenFactor); + gContext.mScaleValueOrigin = makeVect( + gContext.mModelSource.v.right.Length(), gContext.mModelSource.v.up.Length(), gContext.mModelSource.v.dir.Length()); + gContext.mSaveMousePosx = io.MousePos.x; + } + } + // scale + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsScaleType(gContext.mCurrentOperation)) + { #if IMGUI_VERSION_NUM >= 18723 ImGui::SetNextFrameWantCaptureMouse(true); #else ImGui::CaptureMouseFromApp(); #endif - } - - if (type == MT_ROTATE_SCREEN) - { - applyRotationLocaly = true; - } - - if (CanActivate() && type != MT_NONE) - { - gContext.mbUsing = true; - gContext.mEditingID = gContext.GetCurrentID(); - gContext.mCurrentOperation = type; - const vec_t rotatePlanNormal[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, -gContext.mCameraDir }; - // pickup plan - if (applyRotationLocaly) + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; + vec_t newOrigin = newPos - gContext.mRelativeOrigin * gContext.mScreenFactor; + vec_t delta = newOrigin - gContext.mModelLocal.v.position; + + // 1 axis constraint + if (gContext.mCurrentOperation >= MT_SCALE_X && gContext.mCurrentOperation <= MT_SCALE_Z) { - gContext.mTranslationPlan = BuildPlan(gContext.mModel.v.position, rotatePlanNormal[type - MT_ROTATE_X]); + int axisIndex = gContext.mCurrentOperation - MT_SCALE_X; + const vec_t& axisValue = *(vec_t*)&gContext.mModelLocal.m[axisIndex]; + float lengthOnAxis = Dot(axisValue, delta); + delta = axisValue * lengthOnAxis; + + vec_t baseVector = gContext.mTranslationPlanOrigin - gContext.mModelLocal.v.position; + float ratio = Dot(axisValue, baseVector + delta) / Dot(axisValue, baseVector); + + gContext.mScale[axisIndex] = max(ratio, 0.001f); } else { - gContext.mTranslationPlan = BuildPlan(gContext.mModelSource.v.position, directionUnary[type - MT_ROTATE_X]); + float scaleDelta = (io.MousePos.x - gContext.mSaveMousePosx) * 0.01f; + gContext.mScale.Set(max(1.f + scaleDelta, 0.001f)); } - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); - vec_t localPos = gContext.mRayOrigin + gContext.mRayVector * len - gContext.mModel.v.position; - gContext.mRotationVectorSource = Normalized(localPos); - gContext.mRotationAngleOrigin = ComputeAngleOnPlan(); - } - } - - // rotation - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsRotateType(gContext.mCurrentOperation)) - { + // snap + if (snap) + { + float scaleSnap[] = { snap[0], snap[0], snap[0] }; + ComputeSnap(gContext.mScale, scaleSnap); + } + + // no 0 allowed + for (int i = 0; i < 3; i++) + gContext.mScale[i] = max(gContext.mScale[i], 0.001f); + + if (gContext.mScaleLast != gContext.mScale) + { + modified = true; + } + gContext.mScaleLast = gContext.mScale; + + // compute matrix & delta + matrix_t deltaMatrixScale; + deltaMatrixScale.Scale(gContext.mScale * gContext.mScaleValueOrigin); + + matrix_t res = deltaMatrixScale * gContext.mModelLocal; + *(matrix_t*)matrix = res; + + if (deltaMatrix) + { + vec_t deltaScale = gContext.mScale * gContext.mScaleValueOrigin; + + vec_t originalScaleDivider; + originalScaleDivider.x = 1 / gContext.mModelScaleOrigin.x; + originalScaleDivider.y = 1 / gContext.mModelScaleOrigin.y; + originalScaleDivider.z = 1 / gContext.mModelScaleOrigin.z; + + deltaScale = deltaScale * originalScaleDivider; + + deltaMatrixScale.Scale(deltaScale); + memcpy(deltaMatrix, deltaMatrixScale.m16, sizeof(float) * 16); + } + + if (!io.MouseDown[0]) + { + gContext.mbUsing = false; + gContext.mScale.Set(1.f, 1.f, 1.f); + } + + type = gContext.mCurrentOperation; + } + return modified; + } + + static bool HandleRotation(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) + { + if (!Intersects(op, ROTATE) || type != MT_NONE || !gContext.mbMouseOver) + { + return false; + } + ImGuiIO& io = ImGui::GetIO(); + bool applyRotationLocaly = gContext.mMode == LOCAL; + bool modified = false; + + if (!gContext.mbUsing) + { + type = gContext.mbOverGizmoHotspot ? MT_NONE : GetRotateType(op); + gContext.mbOverGizmoHotspot |= type != MT_NONE; + + if (type != MT_NONE) + { #if IMGUI_VERSION_NUM >= 18723 - ImGui::SetNextFrameWantCaptureMouse(true); + ImGui::SetNextFrameWantCaptureMouse(true); #else - ImGui::CaptureMouseFromApp(); + ImGui::CaptureMouseFromApp(); #endif - gContext.mRotationAngle = ComputeAngleOnPlan(); - if (snap) - { - float snapInRadian = snap[0] * DEG2RAD; - ComputeSnap(&gContext.mRotationAngle, snapInRadian); - } - vec_t rotationAxisLocalSpace; - - rotationAxisLocalSpace.TransformVector(makeVect(gContext.mTranslationPlan.x, gContext.mTranslationPlan.y, gContext.mTranslationPlan.z, 0.f), gContext.mModelInverse); - rotationAxisLocalSpace.Normalize(); - - matrix_t deltaRotation; - deltaRotation.RotationAxis(rotationAxisLocalSpace, gContext.mRotationAngle - gContext.mRotationAngleOrigin); - if (gContext.mRotationAngle != gContext.mRotationAngleOrigin) - { - modified = true; - } - gContext.mRotationAngleOrigin = gContext.mRotationAngle; - - matrix_t scaleOrigin; - scaleOrigin.Scale(gContext.mModelScaleOrigin); - - if (applyRotationLocaly) - { - *(matrix_t*)matrix = scaleOrigin * deltaRotation * gContext.mModelLocal; - } - else - { - matrix_t res = gContext.mModelSource; - res.v.position.Set(0.f); - - *(matrix_t*)matrix = res * deltaRotation; - ((matrix_t*)matrix)->v.position = gContext.mModelSource.v.position; - } - - if (deltaMatrix) - { - *(matrix_t*)deltaMatrix = gContext.mModelInverse * deltaRotation * gContext.mModel; - } - - if (!io.MouseDown[0]) - { - gContext.mbUsing = false; - gContext.mEditingID = -1; - } - type = gContext.mCurrentOperation; - } - return modified; - } - - void DecomposeMatrixToComponents(const float* matrix, float* translation, float* rotation, float* scale) - { - matrix_t mat = *(matrix_t*)matrix; - - scale[0] = mat.v.right.Length(); - scale[1] = mat.v.up.Length(); - scale[2] = mat.v.dir.Length(); - - mat.OrthoNormalize(); - - rotation[0] = RAD2DEG * atan2f(mat.m[1][2], mat.m[2][2]); - rotation[1] = RAD2DEG * atan2f(-mat.m[0][2], sqrtf(mat.m[1][2] * mat.m[1][2] + mat.m[2][2] * mat.m[2][2])); - rotation[2] = RAD2DEG * atan2f(mat.m[0][1], mat.m[0][0]); - - translation[0] = mat.v.position.x; - translation[1] = mat.v.position.y; - translation[2] = mat.v.position.z; - } - - void RecomposeMatrixFromComponents(const float* translation, const float* rotation, const float* scale, float* matrix) - { - matrix_t& mat = *(matrix_t*)matrix; - - matrix_t rot[3]; - for (int i = 0; i < 3; i++) - { - rot[i].RotationAxis(directionUnary[i], rotation[i] * DEG2RAD); - } - - mat = rot[0] * rot[1] * rot[2]; - - float validScale[3]; - for (int i = 0; i < 3; i++) - { - if (fabsf(scale[i]) < FLT_EPSILON) - { - validScale[i] = 0.001f; - } - else - { - validScale[i] = scale[i]; - } - } - mat.v.right *= validScale[0]; - mat.v.up *= validScale[1]; - mat.v.dir *= validScale[2]; - mat.v.position.Set(translation[0], translation[1], translation[2], 1.f); - } - - void SetAlternativeWindow(ImGuiWindow* window) - { - gContext.mAlternativeWindow = window; - } - - void SetID(int id) - { - if (gContext.mIDStack.empty()) - { - gContext.mIDStack.push_back(-1); - } - gContext.mIDStack.back() = id; - } - - ImGuiID GetID(const char* str, const char* str_end) - { - ImGuiID seed = gContext.GetCurrentID(); - ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); - return id; - } - - ImGuiID GetID(const char* str) - { - return GetID(str, nullptr); - } - - ImGuiID GetID(const void* ptr) - { - ImGuiID seed = gContext.GetCurrentID(); - ImGuiID id = ImHashData(&ptr, sizeof(void*), seed); - return id; - } - - ImGuiID GetID(int n) - { - ImGuiID seed = gContext.GetCurrentID(); - ImGuiID id = ImHashData(&n, sizeof(n), seed); - return id; - } - - void PushID(const char* str_id) - { - ImGuiID id = GetID(str_id); - gContext.mIDStack.push_back(id); - } - - void PushID(const char* str_id_begin, const char* str_id_end) - { - ImGuiID id = GetID(str_id_begin, str_id_end); - gContext.mIDStack.push_back(id); - } - - void PushID(const void* ptr_id) - { - ImGuiID id = GetID(ptr_id); - gContext.mIDStack.push_back(id); - } - - void PushID(int int_id) - { - ImGuiID id = GetID(int_id); - gContext.mIDStack.push_back(id); - } - - void PopID() - { - IM_ASSERT(gContext.mIDStack.Size > 1); // Too many PopID(), or could be popping in a wrong/different window? - gContext.mIDStack.pop_back(); - } - - void AllowAxisFlip(bool value) - { - gContext.mAllowAxisFlip = value; - } - - void SetAxisLimit(float value) - { - gContext.mAxisLimit=value; - } - - void SetAxisMask(bool x, bool y, bool z) - { - gContext.mAxisMask = (x ? 1 : 0) + (y ? 2 : 0) + (z ? 4 : 0); - } - - void SetPlaneLimit(float value) - { - gContext.mPlaneLimit = value; - } - - bool IsOver(float* position, float pixelRadius) - { - const ImGuiIO& io = ImGui::GetIO(); - - float radius = sqrtf((ImLengthSqr(worldToPos({ position[0], position[1], position[2], 0.0f }, gContext.mViewProjection) - io.MousePos))); - return radius < pixelRadius; - } - - bool Manipulate(const float* view, const float* projection, OPERATION operation, MODE mode, float* matrix, float* deltaMatrix, const float* snap, const float* localBounds, const float* boundsSnap) - { - gContext.mDrawList->PushClipRect (ImVec2 (gContext.mX, gContext.mY), ImVec2 (gContext.mX + gContext.mWidth, gContext.mY + gContext.mHeight), false); - - // Scale is always local or matrix will be skewed when applying world scale or oriented matrix - ComputeContext(view, projection, matrix, (operation & SCALE) ? LOCAL : mode); - - // set delta to identity - if (deltaMatrix) - { - ((matrix_t*)deltaMatrix)->SetToIdentity(); - } - - // behind camera - vec_t camSpacePosition; - camSpacePosition.TransformPoint(makeVect(0.f, 0.f, 0.f), gContext.mMVP); - if (!gContext.mIsOrthographic && camSpacePosition.z < 0.001f && !gContext.mbUsing) - { - return false; - } - - // -- - int type = MT_NONE; - bool manipulated = false; - if (gContext.mbEnable) - { - if (!gContext.mbUsingBounds) - { - manipulated = HandleTranslation(matrix, deltaMatrix, operation, type, snap) || - HandleScale(matrix, deltaMatrix, operation, type, snap) || - HandleRotation(matrix, deltaMatrix, operation, type, snap); - } - } - - if (localBounds && !gContext.mbUsing) - { - HandleAndDrawLocalBounds(localBounds, (matrix_t*)matrix, boundsSnap, operation); - } - - gContext.mOperation = operation; - if (!gContext.mbUsingBounds) - { - DrawRotationGizmo(operation, type); - DrawTranslationGizmo(operation, type); - DrawScaleGizmo(operation, type); - DrawScaleUniveralGizmo(operation, type); - } - - gContext.mDrawList->PopClipRect (); - return manipulated; - } - - void SetGizmoSizeClipSpace(float value) - { - gContext.mGizmoSizeClipSpace = value; - } - - /////////////////////////////////////////////////////////////////////////////////////////////////// - void ComputeFrustumPlanes(vec_t* frustum, const float* clip) - { - frustum[0].x = clip[3] - clip[0]; - frustum[0].y = clip[7] - clip[4]; - frustum[0].z = clip[11] - clip[8]; - frustum[0].w = clip[15] - clip[12]; - - frustum[1].x = clip[3] + clip[0]; - frustum[1].y = clip[7] + clip[4]; - frustum[1].z = clip[11] + clip[8]; - frustum[1].w = clip[15] + clip[12]; - - frustum[2].x = clip[3] + clip[1]; - frustum[2].y = clip[7] + clip[5]; - frustum[2].z = clip[11] + clip[9]; - frustum[2].w = clip[15] + clip[13]; - - frustum[3].x = clip[3] - clip[1]; - frustum[3].y = clip[7] - clip[5]; - frustum[3].z = clip[11] - clip[9]; - frustum[3].w = clip[15] - clip[13]; - - frustum[4].x = clip[3] - clip[2]; - frustum[4].y = clip[7] - clip[6]; - frustum[4].z = clip[11] - clip[10]; - frustum[4].w = clip[15] - clip[14]; - - frustum[5].x = clip[3] + clip[2]; - frustum[5].y = clip[7] + clip[6]; - frustum[5].z = clip[11] + clip[10]; - frustum[5].w = clip[15] + clip[14]; - - for (int i = 0; i < 6; i++) - { - frustum[i].Normalize(); - } - } - - void DrawCubes(const float* view, const float* projection, const float* matrices, int matrixCount) - { - matrix_t viewInverse; - viewInverse.Inverse(*(matrix_t*)view); - - struct CubeFace - { - float z; - ImVec2 faceCoordsScreen[4]; - ImU32 color; - }; - CubeFace* faces = (CubeFace*)_malloca(sizeof(CubeFace) * matrixCount * 6); - - if (!faces) - { - return; - } - - vec_t frustum[6]; - matrix_t viewProjection = *(matrix_t*)view * *(matrix_t*)projection; - ComputeFrustumPlanes(frustum, viewProjection.m16); - - int cubeFaceCount = 0; - for (int cube = 0; cube < matrixCount; cube++) - { - const float* matrix = &matrices[cube * 16]; - - matrix_t res = *(matrix_t*)matrix * *(matrix_t*)view * *(matrix_t*)projection; - - for (int iFace = 0; iFace < 6; iFace++) - { - const int normalIndex = (iFace % 3); - const int perpXIndex = (normalIndex + 1) % 3; - const int perpYIndex = (normalIndex + 2) % 3; - const float invert = (iFace > 2) ? -1.f : 1.f; - - const vec_t faceCoords[4] = { directionUnary[normalIndex] + directionUnary[perpXIndex] + directionUnary[perpYIndex], - directionUnary[normalIndex] + directionUnary[perpXIndex] - directionUnary[perpYIndex], - directionUnary[normalIndex] - directionUnary[perpXIndex] - directionUnary[perpYIndex], - directionUnary[normalIndex] - directionUnary[perpXIndex] + directionUnary[perpYIndex], - }; - - // clipping - /* - bool skipFace = false; - for (unsigned int iCoord = 0; iCoord < 4; iCoord++) - { - vec_t camSpacePosition; - camSpacePosition.TransformPoint(faceCoords[iCoord] * 0.5f * invert, res); - if (camSpacePosition.z < 0.001f) - { - skipFace = true; - break; - } } - if (skipFace) + + if (type == MT_ROTATE_SCREEN) { - continue; + applyRotationLocaly = true; } - */ - vec_t centerPosition, centerPositionVP; - centerPosition.TransformPoint(directionUnary[normalIndex] * 0.5f * invert, *(matrix_t*)matrix); - centerPositionVP.TransformPoint(directionUnary[normalIndex] * 0.5f * invert, res); - bool inFrustum = true; - for (int iFrustum = 0; iFrustum < 6; iFrustum++) + if (CanActivate() && type != MT_NONE) { - float dist = DistanceToPlane(centerPosition, frustum[iFrustum]); - if (dist < 0.f) - { - inFrustum = false; - break; - } + gContext.mbUsing = true; + gContext.mEditingID = gContext.GetCurrentID(); + gContext.mCurrentOperation = type; + const vec_t rotatePlanNormal[] = { + gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, -gContext.mCameraDir + }; + // pickup plan + if (applyRotationLocaly) + { + gContext.mTranslationPlan = BuildPlan(gContext.mModel.v.position, rotatePlanNormal[type - MT_ROTATE_X]); + } + else + { + gContext.mTranslationPlan = BuildPlan(gContext.mModelSource.v.position, directionUnary[type - MT_ROTATE_X]); + } + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + vec_t localPos = gContext.mRayOrigin + gContext.mRayVector * len - gContext.mModel.v.position; + gContext.mRotationVectorSource = Normalized(localPos); + gContext.mRotationAngleOrigin = ComputeAngleOnPlan(); } + } - if (!inFrustum) - { - continue; - } - CubeFace& cubeFace = faces[cubeFaceCount]; - - // 3D->2D - //ImVec2 faceCoordsScreen[4]; - for (unsigned int iCoord = 0; iCoord < 4; iCoord++) - { - cubeFace.faceCoordsScreen[iCoord] = worldToPos(faceCoords[iCoord] * 0.5f * invert, res); - } - - ImU32 directionColor = GetColorU32(DIRECTION_X + normalIndex); - cubeFace.color = directionColor | IM_COL32(0x80, 0x80, 0x80, 0); - - cubeFace.z = centerPositionVP.z / centerPositionVP.w; - cubeFaceCount++; - } - } - qsort(faces, cubeFaceCount, sizeof(CubeFace), [](void const* _a, void const* _b) { - CubeFace* a = (CubeFace*)_a; - CubeFace* b = (CubeFace*)_b; - if (a->z < b->z) - { - return 1; - } - return -1; - }); - // draw face with lighter color - for (int iFace = 0; iFace < cubeFaceCount; iFace++) - { - const CubeFace& cubeFace = faces[iFace]; - gContext.mDrawList->AddConvexPolyFilled(cubeFace.faceCoordsScreen, 4, cubeFace.color); - } - - _freea(faces); - } - - void DrawGrid(const float* view, const float* projection, const float* matrix, const float gridSize) - { - matrix_t viewProjection = *(matrix_t*)view * *(matrix_t*)projection; - vec_t frustum[6]; - ComputeFrustumPlanes(frustum, viewProjection.m16); - matrix_t res = *(matrix_t*)matrix * viewProjection; - - for (float f = -gridSize; f <= gridSize; f += 1.f) - { - for (int dir = 0; dir < 2; dir++) - { - vec_t ptA = makeVect(dir ? -gridSize : f, 0.f, dir ? f : -gridSize); - vec_t ptB = makeVect(dir ? gridSize : f, 0.f, dir ? f : gridSize); - bool visible = true; - for (int i = 0; i < 6; i++) - { - float dA = DistanceToPlane(ptA, frustum[i]); - float dB = DistanceToPlane(ptB, frustum[i]); - if (dA < 0.f && dB < 0.f) - { - visible = false; - break; - } - if (dA > 0.f && dB > 0.f) - { - continue; - } - if (dA < 0.f) - { - float len = fabsf(dA - dB); - float t = fabsf(dA) / len; - ptA.Lerp(ptB, t); - } - if (dB < 0.f) - { - float len = fabsf(dB - dA); - float t = fabsf(dB) / len; - ptB.Lerp(ptA, t); - } + // rotation + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsRotateType(gContext.mCurrentOperation)) + { +#if IMGUI_VERSION_NUM >= 18723 + ImGui::SetNextFrameWantCaptureMouse(true); +#else + ImGui::CaptureMouseFromApp(); +#endif + gContext.mRotationAngle = ComputeAngleOnPlan(); + if (snap) + { + float snapInRadian = snap[0] * DEG2RAD; + ComputeSnap(&gContext.mRotationAngle, snapInRadian); } - if (visible) - { - ImU32 col = IM_COL32(0x80, 0x80, 0x80, 0xFF); - col = (fmodf(fabsf(f), 10.f) < FLT_EPSILON) ? IM_COL32(0x90, 0x90, 0x90, 0xFF) : col; - col = (fabsf(f) < FLT_EPSILON) ? IM_COL32(0x40, 0x40, 0x40, 0xFF): col; - - float thickness = 1.f; - thickness = (fmodf(fabsf(f), 10.f) < FLT_EPSILON) ? 1.5f : thickness; - thickness = (fabsf(f) < FLT_EPSILON) ? 2.3f : thickness; - - gContext.mDrawList->AddLine(worldToPos(ptA, res), worldToPos(ptB, res), col, thickness); - } - } - } - } - - void ViewManipulate(float* view, const float* projection, OPERATION operation, MODE mode, float* matrix, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor) - { - // Scale is always local or matrix will be skewed when applying world scale or oriented matrix - ComputeContext(view, projection, matrix, (operation & SCALE) ? LOCAL : mode); - ViewManipulate(view, length, position, size, backgroundColor); - } - - void ViewManipulate(float* view, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor) - { - static bool isDraging = false; - static bool isClicking = false; - static vec_t interpolationUp; - static vec_t interpolationDir; - static int interpolationFrames = 0; - const vec_t referenceUp = makeVect(0.f, 1.f, 0.f); - - matrix_t svgView, svgProjection; - svgView = gContext.mViewMat; - svgProjection = gContext.mProjectionMat; - - ImGuiIO& io = ImGui::GetIO(); - gContext.mDrawList->AddRectFilled(position, position + size, backgroundColor); - matrix_t viewInverse; - viewInverse.Inverse(*(matrix_t*)view); - - const vec_t camTarget = viewInverse.v.position - viewInverse.v.dir * length; - - // view/projection matrices - const float distance = 3.f; - matrix_t cubeProjection, cubeView; - float fov = acosf(distance / (sqrtf(distance * distance + 3.f))) * RAD2DEG; - Perspective(fov / sqrtf(2.f), size.x / size.y, 0.01f, 1000.f, cubeProjection.m16); - - vec_t dir = makeVect(viewInverse.m[2][0], viewInverse.m[2][1], viewInverse.m[2][2]); - vec_t up = makeVect(viewInverse.m[1][0], viewInverse.m[1][1], viewInverse.m[1][2]); - vec_t eye = dir * distance; - vec_t zero = makeVect(0.f, 0.f); - LookAt(&eye.x, &zero.x, &up.x, cubeView.m16); - - // set context - gContext.mViewMat = cubeView; - gContext.mProjectionMat = cubeProjection; - ComputeCameraRay(gContext.mRayOrigin, gContext.mRayVector, position, size); - - const matrix_t res = cubeView * cubeProjection; - - // panels - static const ImVec2 panelPosition[9] = { ImVec2(0.75f,0.75f), ImVec2(0.25f, 0.75f), ImVec2(0.f, 0.75f), - ImVec2(0.75f, 0.25f), ImVec2(0.25f, 0.25f), ImVec2(0.f, 0.25f), - ImVec2(0.75f, 0.f), ImVec2(0.25f, 0.f), ImVec2(0.f, 0.f) }; - - static const ImVec2 panelSize[9] = { ImVec2(0.25f,0.25f), ImVec2(0.5f, 0.25f), ImVec2(0.25f, 0.25f), - ImVec2(0.25f, 0.5f), ImVec2(0.5f, 0.5f), ImVec2(0.25f, 0.5f), - ImVec2(0.25f, 0.25f), ImVec2(0.5f, 0.25f), ImVec2(0.25f, 0.25f) }; - - // tag faces - bool boxes[27]{}; - static int overBox = -1; - for (int iPass = 0; iPass < 2; iPass++) - { - for (int iFace = 0; iFace < 6; iFace++) - { - const int normalIndex = (iFace % 3); - const int perpXIndex = (normalIndex + 1) % 3; - const int perpYIndex = (normalIndex + 2) % 3; - const float invert = (iFace > 2) ? -1.f : 1.f; - const vec_t indexVectorX = directionUnary[perpXIndex] * invert; - const vec_t indexVectorY = directionUnary[perpYIndex] * invert; - const vec_t boxOrigin = directionUnary[normalIndex] * -invert - indexVectorX - indexVectorY; - - // plan local space - const vec_t n = directionUnary[normalIndex] * invert; - vec_t viewSpaceNormal = n; - vec_t viewSpacePoint = n * 0.5f; - viewSpaceNormal.TransformVector(cubeView); - viewSpaceNormal.Normalize(); - viewSpacePoint.TransformPoint(cubeView); - const vec_t viewSpaceFacePlan = BuildPlan(viewSpacePoint, viewSpaceNormal); - - // back face culling - if (viewSpaceFacePlan.w > 0.f) - { - continue; - } - - const vec_t facePlan = BuildPlan(n * 0.5f, n); - - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, facePlan); - vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len - (n * 0.5f); - - float localx = Dot(directionUnary[perpXIndex], posOnPlan) * invert + 0.5f; - float localy = Dot(directionUnary[perpYIndex], posOnPlan) * invert + 0.5f; - - // panels - const vec_t dx = directionUnary[perpXIndex]; - const vec_t dy = directionUnary[perpYIndex]; - const vec_t origin = directionUnary[normalIndex] - dx - dy; - for (int iPanel = 0; iPanel < 9; iPanel++) - { - vec_t boxCoord = boxOrigin + indexVectorX * float(iPanel % 3) + indexVectorY * float(iPanel / 3) + makeVect(1.f, 1.f, 1.f); - const ImVec2 p = panelPosition[iPanel] * 2.f; - const ImVec2 s = panelSize[iPanel] * 2.f; - ImVec2 faceCoordsScreen[4]; - vec_t panelPos[4] = { dx * p.x + dy * p.y, - dx * p.x + dy * (p.y + s.y), - dx * (p.x + s.x) + dy * (p.y + s.y), - dx * (p.x + s.x) + dy * p.y }; - - for (unsigned int iCoord = 0; iCoord < 4; iCoord++) - { - faceCoordsScreen[iCoord] = worldToPos((panelPos[iCoord] + origin) * 0.5f * invert, res, position, size); - } + vec_t rotationAxisLocalSpace; - const ImVec2 panelCorners[2] = { panelPosition[iPanel], panelPosition[iPanel] + panelSize[iPanel] }; - bool insidePanel = localx > panelCorners[0].x && localx < panelCorners[1].x && localy > panelCorners[0].y && localy < panelCorners[1].y; - int boxCoordInt = int(boxCoord.x * 9.f + boxCoord.y * 3.f + boxCoord.z); - IM_ASSERT(boxCoordInt < 27); - boxes[boxCoordInt] |= insidePanel && (!isDraging) && gContext.mbMouseOver; + rotationAxisLocalSpace.TransformVector( + makeVect(gContext.mTranslationPlan.x, gContext.mTranslationPlan.y, gContext.mTranslationPlan.z, 0.f), + gContext.mModelInverse); + rotationAxisLocalSpace.Normalize(); - // draw face with lighter color - if (iPass) - { - ImU32 directionColor = GetColorU32(DIRECTION_X + normalIndex); - gContext.mDrawList->AddConvexPolyFilled(faceCoordsScreen, 4, (directionColor | IM_COL32(0x80, 0x80, 0x80, 0x80)) | (gContext.mIsViewManipulatorHovered ? IM_COL32(0x08, 0x08, 0x08, 0) : 0)); - if (boxes[boxCoordInt]) - { - gContext.mDrawList->AddConvexPolyFilled(faceCoordsScreen, 4, IM_COL32(0xF0, 0xA0, 0x60, 0x80)); - - if (io.MouseDown[0] && !isClicking && !isDraging && GImGui->ActiveId == 0) { - overBox = boxCoordInt; - isClicking = true; - isDraging = true; - } - } - } + matrix_t deltaRotation; + deltaRotation.RotationAxis(rotationAxisLocalSpace, gContext.mRotationAngle - gContext.mRotationAngleOrigin); + if (gContext.mRotationAngle != gContext.mRotationAngleOrigin) + { + modified = true; } - } - } - if (interpolationFrames) - { - interpolationFrames--; - vec_t newDir = viewInverse.v.dir; - newDir.Lerp(interpolationDir, 0.2f); - newDir.Normalize(); - - vec_t newUp = viewInverse.v.up; - newUp.Lerp(interpolationUp, 0.3f); - newUp.Normalize(); - newUp = interpolationUp; - vec_t newEye = camTarget + newDir * length; - LookAt(&newEye.x, &camTarget.x, &newUp.x, view); - } - gContext.mIsViewManipulatorHovered = gContext.mbMouseOver && ImRect(position, position + size).Contains(io.MousePos); - - if (io.MouseDown[0] && (fabsf(io.MouseDelta[0]) || fabsf(io.MouseDelta[1])) && isClicking) - { - isClicking = false; - } - - if (!io.MouseDown[0]) - { - if (isClicking) - { - // apply new view direction - int cx = overBox / 9; - int cy = (overBox - cx * 9) / 3; - int cz = overBox % 3; - interpolationDir = makeVect(1.f - (float)cx, 1.f - (float)cy, 1.f - (float)cz); - interpolationDir.Normalize(); - - if (fabsf(Dot(interpolationDir, referenceUp)) > 1.0f - 0.01f) - { - vec_t right = viewInverse.v.right; - if (fabsf(right.x) > fabsf(right.z)) - { - right.z = 0.f; - } - else - { - right.x = 0.f; - } - right.Normalize(); - interpolationUp = Cross(interpolationDir, right); - interpolationUp.Normalize(); + gContext.mRotationAngleOrigin = gContext.mRotationAngle; + + matrix_t scaleOrigin; + scaleOrigin.Scale(gContext.mModelScaleOrigin); + + if (applyRotationLocaly) + { + *(matrix_t*)matrix = scaleOrigin * deltaRotation * gContext.mModelLocal; } else { - interpolationUp = referenceUp; + matrix_t res = gContext.mModelSource; + res.v.position.Set(0.f); + + *(matrix_t*)matrix = res * deltaRotation; + ((matrix_t*)matrix)->v.position = gContext.mModelSource.v.position; } - interpolationFrames = 40; - } - isClicking = false; - isDraging = false; - } + if (deltaMatrix) + { + *(matrix_t*)deltaMatrix = gContext.mModelInverse * deltaRotation * gContext.mModel; + } + if (!io.MouseDown[0]) + { + gContext.mbUsing = false; + gContext.mEditingID = -1; + } + type = gContext.mCurrentOperation; + } + return modified; + } + + void DecomposeMatrixToComponents(const float* matrix, float* translation, float* rotation, float* scale) + { + matrix_t mat = *(matrix_t*)matrix; + + scale[0] = mat.v.right.Length(); + scale[1] = mat.v.up.Length(); + scale[2] = mat.v.dir.Length(); + + mat.OrthoNormalize(); + + rotation[0] = RAD2DEG * atan2f(mat.m[1][2], mat.m[2][2]); + rotation[1] = RAD2DEG * atan2f(-mat.m[0][2], sqrtf(mat.m[1][2] * mat.m[1][2] + mat.m[2][2] * mat.m[2][2])); + rotation[2] = RAD2DEG * atan2f(mat.m[0][1], mat.m[0][0]); + + translation[0] = mat.v.position.x; + translation[1] = mat.v.position.y; + translation[2] = mat.v.position.z; + } + + void RecomposeMatrixFromComponents(const float* translation, const float* rotation, const float* scale, float* matrix) + { + matrix_t& mat = *(matrix_t*)matrix; + + matrix_t rot[3]; + for (int i = 0; i < 3; i++) + { + rot[i].RotationAxis(directionUnary[i], rotation[i] * DEG2RAD); + } + + mat = rot[0] * rot[1] * rot[2]; + + float validScale[3]; + for (int i = 0; i < 3; i++) + { + if (fabsf(scale[i]) < FLT_EPSILON) + { + validScale[i] = 0.001f; + } + else + { + validScale[i] = scale[i]; + } + } + mat.v.right *= validScale[0]; + mat.v.up *= validScale[1]; + mat.v.dir *= validScale[2]; + mat.v.position.Set(translation[0], translation[1], translation[2], 1.f); + } + + void SetAlternativeWindow(ImGuiWindow* window) + { + gContext.mAlternativeWindow = window; + } + + void SetID(int id) + { + if (gContext.mIDStack.empty()) + { + gContext.mIDStack.push_back(-1); + } + gContext.mIDStack.back() = id; + } + + ImGuiID GetID(const char* str, const char* str_end) + { + ImGuiID seed = gContext.GetCurrentID(); + ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); + return id; + } + + ImGuiID GetID(const char* str) + { + return GetID(str, nullptr); + } + + ImGuiID GetID(const void* ptr) + { + ImGuiID seed = gContext.GetCurrentID(); + ImGuiID id = ImHashData(&ptr, sizeof(void*), seed); + return id; + } + + ImGuiID GetID(int n) + { + ImGuiID seed = gContext.GetCurrentID(); + ImGuiID id = ImHashData(&n, sizeof(n), seed); + return id; + } + + void PushID(const char* str_id) + { + ImGuiID id = GetID(str_id); + gContext.mIDStack.push_back(id); + } + + void PushID(const char* str_id_begin, const char* str_id_end) + { + ImGuiID id = GetID(str_id_begin, str_id_end); + gContext.mIDStack.push_back(id); + } + + void PushID(const void* ptr_id) + { + ImGuiID id = GetID(ptr_id); + gContext.mIDStack.push_back(id); + } + + void PushID(int int_id) + { + ImGuiID id = GetID(int_id); + gContext.mIDStack.push_back(id); + } + + void PopID() + { + IM_ASSERT(gContext.mIDStack.Size > 1); // Too many PopID(), or could be popping in a wrong/different window? + gContext.mIDStack.pop_back(); + } + + void AllowAxisFlip(bool value) + { + gContext.mAllowAxisFlip = value; + } + + void SetAxisLimit(float value) + { + gContext.mAxisLimit = value; + } + + void SetAxisMask(bool x, bool y, bool z) + { + gContext.mAxisMask = (x ? 1 : 0) + (y ? 2 : 0) + (z ? 4 : 0); + } + + void SetPlaneLimit(float value) + { + gContext.mPlaneLimit = value; + } + + bool IsOver(float* position, float pixelRadius) + { + const ImGuiIO& io = ImGui::GetIO(); + + float radius = + sqrtf((ImLengthSqr(worldToPos({ position[0], position[1], position[2], 0.0f }, gContext.mViewProjection) - io.MousePos))); + return radius < pixelRadius; + } + + bool Manipulate( + const float* view, + const float* projection, + OPERATION operation, + MODE mode, + float* matrix, + float* deltaMatrix, + const float* snap, + const float* localBounds, + const float* boundsSnap) + { + gContext.mDrawList->PushClipRect( + ImVec2(gContext.mX, gContext.mY), ImVec2(gContext.mX + gContext.mWidth, gContext.mY + gContext.mHeight), false); + + // Scale is always local or matrix will be skewed when applying world scale or oriented matrix + ComputeContext(view, projection, matrix, (operation & SCALE) ? LOCAL : mode); + + // set delta to identity + if (deltaMatrix) + { + ((matrix_t*)deltaMatrix)->SetToIdentity(); + } + + // behind camera + vec_t camSpacePosition; + camSpacePosition.TransformPoint(makeVect(0.f, 0.f, 0.f), gContext.mMVP); + if (!gContext.mIsOrthographic && camSpacePosition.z < 0.001f && !gContext.mbUsing) + { + return false; + } + + // -- + int type = MT_NONE; + bool manipulated = false; + if (gContext.mbEnable) + { + if (!gContext.mbUsingBounds) + { + manipulated = HandleTranslation(matrix, deltaMatrix, operation, type, snap) || + HandleScale(matrix, deltaMatrix, operation, type, snap) || HandleRotation(matrix, deltaMatrix, operation, type, snap); + } + } + + if (localBounds && !gContext.mbUsing) + { + HandleAndDrawLocalBounds(localBounds, (matrix_t*)matrix, boundsSnap, operation); + } + + gContext.mOperation = operation; + if (!gContext.mbUsingBounds) + { + DrawRotationGizmo(operation, type); + DrawTranslationGizmo(operation, type); + DrawScaleGizmo(operation, type); + DrawScaleUniveralGizmo(operation, type); + } + + gContext.mDrawList->PopClipRect(); + return manipulated; + } + + void SetGizmoSizeClipSpace(float value) + { + gContext.mGizmoSizeClipSpace = value; + } + + /////////////////////////////////////////////////////////////////////////////////////////////////// + void ComputeFrustumPlanes(vec_t* frustum, const float* clip) + { + frustum[0].x = clip[3] - clip[0]; + frustum[0].y = clip[7] - clip[4]; + frustum[0].z = clip[11] - clip[8]; + frustum[0].w = clip[15] - clip[12]; + + frustum[1].x = clip[3] + clip[0]; + frustum[1].y = clip[7] + clip[4]; + frustum[1].z = clip[11] + clip[8]; + frustum[1].w = clip[15] + clip[12]; + + frustum[2].x = clip[3] + clip[1]; + frustum[2].y = clip[7] + clip[5]; + frustum[2].z = clip[11] + clip[9]; + frustum[2].w = clip[15] + clip[13]; + + frustum[3].x = clip[3] - clip[1]; + frustum[3].y = clip[7] - clip[5]; + frustum[3].z = clip[11] - clip[9]; + frustum[3].w = clip[15] - clip[13]; + + frustum[4].x = clip[3] - clip[2]; + frustum[4].y = clip[7] - clip[6]; + frustum[4].z = clip[11] - clip[10]; + frustum[4].w = clip[15] - clip[14]; + + frustum[5].x = clip[3] + clip[2]; + frustum[5].y = clip[7] + clip[6]; + frustum[5].z = clip[11] + clip[10]; + frustum[5].w = clip[15] + clip[14]; + + for (int i = 0; i < 6; i++) + { + frustum[i].Normalize(); + } + } + + void DrawCubes(const float* view, const float* projection, const float* matrices, int matrixCount) + { + matrix_t viewInverse; + viewInverse.Inverse(*(matrix_t*)view); + + struct CubeFace + { + float z; + ImVec2 faceCoordsScreen[4]; + ImU32 color; + }; + CubeFace* faces = (CubeFace*)_malloca(sizeof(CubeFace) * matrixCount * 6); + + if (!faces) + { + return; + } + + vec_t frustum[6]; + matrix_t viewProjection = *(matrix_t*)view * *(matrix_t*)projection; + ComputeFrustumPlanes(frustum, viewProjection.m16); + + int cubeFaceCount = 0; + for (int cube = 0; cube < matrixCount; cube++) + { + const float* matrix = &matrices[cube * 16]; + + matrix_t res = *(matrix_t*)matrix * *(matrix_t*)view * *(matrix_t*)projection; + + for (int iFace = 0; iFace < 6; iFace++) + { + const int normalIndex = (iFace % 3); + const int perpXIndex = (normalIndex + 1) % 3; + const int perpYIndex = (normalIndex + 2) % 3; + const float invert = (iFace > 2) ? -1.f : 1.f; + + const vec_t faceCoords[4] = { + directionUnary[normalIndex] + directionUnary[perpXIndex] + directionUnary[perpYIndex], + directionUnary[normalIndex] + directionUnary[perpXIndex] - directionUnary[perpYIndex], + directionUnary[normalIndex] - directionUnary[perpXIndex] - directionUnary[perpYIndex], + directionUnary[normalIndex] - directionUnary[perpXIndex] + directionUnary[perpYIndex], + }; + + // clipping + /* + bool skipFace = false; + for (unsigned int iCoord = 0; iCoord < 4; iCoord++) + { + vec_t camSpacePosition; + camSpacePosition.TransformPoint(faceCoords[iCoord] * 0.5f * invert, res); + if (camSpacePosition.z < 0.001f) + { + skipFace = true; + break; + } + } + if (skipFace) + { + continue; + } + */ + vec_t centerPosition, centerPositionVP; + centerPosition.TransformPoint(directionUnary[normalIndex] * 0.5f * invert, *(matrix_t*)matrix); + centerPositionVP.TransformPoint(directionUnary[normalIndex] * 0.5f * invert, res); + + bool inFrustum = true; + for (int iFrustum = 0; iFrustum < 6; iFrustum++) + { + float dist = DistanceToPlane(centerPosition, frustum[iFrustum]); + if (dist < 0.f) + { + inFrustum = false; + break; + } + } + + if (!inFrustum) + { + continue; + } + CubeFace& cubeFace = faces[cubeFaceCount]; + + // 3D->2D + // ImVec2 faceCoordsScreen[4]; + for (unsigned int iCoord = 0; iCoord < 4; iCoord++) + { + cubeFace.faceCoordsScreen[iCoord] = worldToPos(faceCoords[iCoord] * 0.5f * invert, res); + } + + ImU32 directionColor = GetColorU32(DIRECTION_X + normalIndex); + cubeFace.color = directionColor | IM_COL32(0x80, 0x80, 0x80, 0); + + cubeFace.z = centerPositionVP.z / centerPositionVP.w; + cubeFaceCount++; + } + } + qsort( + faces, + cubeFaceCount, + sizeof(CubeFace), + [](void const* _a, void const* _b) + { + CubeFace* a = (CubeFace*)_a; + CubeFace* b = (CubeFace*)_b; + if (a->z < b->z) + { + return 1; + } + return -1; + }); + // draw face with lighter color + for (int iFace = 0; iFace < cubeFaceCount; iFace++) + { + const CubeFace& cubeFace = faces[iFace]; + gContext.mDrawList->AddConvexPolyFilled(cubeFace.faceCoordsScreen, 4, cubeFace.color); + } + + _freea(faces); + } + + void DrawGrid(const float* view, const float* projection, const float* matrix, const float gridSize) + { + matrix_t viewProjection = *(matrix_t*)view * *(matrix_t*)projection; + vec_t frustum[6]; + ComputeFrustumPlanes(frustum, viewProjection.m16); + matrix_t res = *(matrix_t*)matrix * viewProjection; + + for (float f = -gridSize; f <= gridSize; f += 1.f) + { + for (int dir = 0; dir < 2; dir++) + { + vec_t ptA = makeVect(dir ? -gridSize : f, 0.f, dir ? f : -gridSize); + vec_t ptB = makeVect(dir ? gridSize : f, 0.f, dir ? f : gridSize); + bool visible = true; + for (int i = 0; i < 6; i++) + { + float dA = DistanceToPlane(ptA, frustum[i]); + float dB = DistanceToPlane(ptB, frustum[i]); + if (dA < 0.f && dB < 0.f) + { + visible = false; + break; + } + if (dA > 0.f && dB > 0.f) + { + continue; + } + if (dA < 0.f) + { + float len = fabsf(dA - dB); + float t = fabsf(dA) / len; + ptA.Lerp(ptB, t); + } + if (dB < 0.f) + { + float len = fabsf(dB - dA); + float t = fabsf(dB) / len; + ptB.Lerp(ptA, t); + } + } + if (visible) + { + ImU32 col = IM_COL32(0x80, 0x80, 0x80, 0xFF); + col = (fmodf(fabsf(f), 10.f) < FLT_EPSILON) ? IM_COL32(0x90, 0x90, 0x90, 0xFF) : col; + col = (fabsf(f) < FLT_EPSILON) ? IM_COL32(0x40, 0x40, 0x40, 0xFF) : col; + + float thickness = 1.f; + thickness = (fmodf(fabsf(f), 10.f) < FLT_EPSILON) ? 1.5f : thickness; + thickness = (fabsf(f) < FLT_EPSILON) ? 2.3f : thickness; + + gContext.mDrawList->AddLine(worldToPos(ptA, res), worldToPos(ptB, res), col, thickness); + } + } + } + } + + void ViewManipulate( + float* view, + const float* projection, + OPERATION operation, + MODE mode, + float* matrix, + float length, + ImVec2 position, + ImVec2 size, + ImU32 backgroundColor) + { + // Scale is always local or matrix will be skewed when applying world scale or oriented matrix + ComputeContext(view, projection, matrix, (operation & SCALE) ? LOCAL : mode); + ViewManipulate(view, length, position, size, backgroundColor); + } + + void ViewManipulate(float* view, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor) + { + static bool isDraging = false; + static bool isClicking = false; + static vec_t interpolationUp; + static vec_t interpolationDir; + static int interpolationFrames = 0; + const vec_t referenceUp = makeVect(0.f, 1.f, 0.f); + + matrix_t svgView, svgProjection; + svgView = gContext.mViewMat; + svgProjection = gContext.mProjectionMat; + + ImGuiIO& io = ImGui::GetIO(); + gContext.mDrawList->AddRectFilled(position, position + size, backgroundColor); + matrix_t viewInverse; + viewInverse.Inverse(*(matrix_t*)view); + + const vec_t camTarget = viewInverse.v.position - viewInverse.v.dir * length; + + // view/projection matrices + const float distance = 3.f; + matrix_t cubeProjection, cubeView; + float fov = acosf(distance / (sqrtf(distance * distance + 3.f))) * RAD2DEG; + Perspective(fov / sqrtf(2.f), size.x / size.y, 0.01f, 1000.f, cubeProjection.m16); + + vec_t dir = makeVect(viewInverse.m[2][0], viewInverse.m[2][1], viewInverse.m[2][2]); + vec_t up = makeVect(viewInverse.m[1][0], viewInverse.m[1][1], viewInverse.m[1][2]); + vec_t eye = dir * distance; + vec_t zero = makeVect(0.f, 0.f); + LookAt(&eye.x, &zero.x, &up.x, cubeView.m16); + + // set context + gContext.mViewMat = cubeView; + gContext.mProjectionMat = cubeProjection; + ComputeCameraRay(gContext.mRayOrigin, gContext.mRayVector, position, size); + + const matrix_t res = cubeView * cubeProjection; + + // panels + static const ImVec2 panelPosition[9] = { ImVec2(0.75f, 0.75f), ImVec2(0.25f, 0.75f), ImVec2(0.f, 0.75f), + ImVec2(0.75f, 0.25f), ImVec2(0.25f, 0.25f), ImVec2(0.f, 0.25f), + ImVec2(0.75f, 0.f), ImVec2(0.25f, 0.f), ImVec2(0.f, 0.f) }; + + static const ImVec2 panelSize[9] = { ImVec2(0.25f, 0.25f), ImVec2(0.5f, 0.25f), ImVec2(0.25f, 0.25f), + ImVec2(0.25f, 0.5f), ImVec2(0.5f, 0.5f), ImVec2(0.25f, 0.5f), + ImVec2(0.25f, 0.25f), ImVec2(0.5f, 0.25f), ImVec2(0.25f, 0.25f) }; + + // tag faces + bool boxes[27]{}; + static int overBox = -1; + for (int iPass = 0; iPass < 2; iPass++) + { + for (int iFace = 0; iFace < 6; iFace++) + { + const int normalIndex = (iFace % 3); + const int perpXIndex = (normalIndex + 1) % 3; + const int perpYIndex = (normalIndex + 2) % 3; + const float invert = (iFace > 2) ? -1.f : 1.f; + const vec_t indexVectorX = directionUnary[perpXIndex] * invert; + const vec_t indexVectorY = directionUnary[perpYIndex] * invert; + const vec_t boxOrigin = directionUnary[normalIndex] * -invert - indexVectorX - indexVectorY; + + // plan local space + const vec_t n = directionUnary[normalIndex] * invert; + vec_t viewSpaceNormal = n; + vec_t viewSpacePoint = n * 0.5f; + viewSpaceNormal.TransformVector(cubeView); + viewSpaceNormal.Normalize(); + viewSpacePoint.TransformPoint(cubeView); + const vec_t viewSpaceFacePlan = BuildPlan(viewSpacePoint, viewSpaceNormal); + + // back face culling + if (viewSpaceFacePlan.w > 0.f) + { + continue; + } + + const vec_t facePlan = BuildPlan(n * 0.5f, n); + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, facePlan); + vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len - (n * 0.5f); + + float localx = Dot(directionUnary[perpXIndex], posOnPlan) * invert + 0.5f; + float localy = Dot(directionUnary[perpYIndex], posOnPlan) * invert + 0.5f; + + // panels + const vec_t dx = directionUnary[perpXIndex]; + const vec_t dy = directionUnary[perpYIndex]; + const vec_t origin = directionUnary[normalIndex] - dx - dy; + for (int iPanel = 0; iPanel < 9; iPanel++) + { + vec_t boxCoord = + boxOrigin + indexVectorX * float(iPanel % 3) + indexVectorY * float(iPanel / 3) + makeVect(1.f, 1.f, 1.f); + const ImVec2 p = panelPosition[iPanel] * 2.f; + const ImVec2 s = panelSize[iPanel] * 2.f; + ImVec2 faceCoordsScreen[4]; + vec_t panelPos[4] = { + dx * p.x + dy * p.y, dx * p.x + dy * (p.y + s.y), dx * (p.x + s.x) + dy * (p.y + s.y), dx * (p.x + s.x) + dy * p.y + }; + + for (unsigned int iCoord = 0; iCoord < 4; iCoord++) + { + faceCoordsScreen[iCoord] = worldToPos((panelPos[iCoord] + origin) * 0.5f * invert, res, position, size); + } + + const ImVec2 panelCorners[2] = { panelPosition[iPanel], panelPosition[iPanel] + panelSize[iPanel] }; + bool insidePanel = localx > panelCorners[0].x && localx < panelCorners[1].x && localy > panelCorners[0].y && + localy < panelCorners[1].y; + int boxCoordInt = int(boxCoord.x * 9.f + boxCoord.y * 3.f + boxCoord.z); + IM_ASSERT(boxCoordInt < 27); + boxes[boxCoordInt] |= insidePanel && (!isDraging) && gContext.mbMouseOver; + + // draw face with lighter color + if (iPass) + { + ImU32 directionColor = GetColorU32(DIRECTION_X + normalIndex); + gContext.mDrawList->AddConvexPolyFilled( + faceCoordsScreen, + 4, + (directionColor | IM_COL32(0x80, 0x80, 0x80, 0x80)) | + (gContext.mIsViewManipulatorHovered ? IM_COL32(0x08, 0x08, 0x08, 0) : 0)); + if (boxes[boxCoordInt]) + { + gContext.mDrawList->AddConvexPolyFilled(faceCoordsScreen, 4, IM_COL32(0xF0, 0xA0, 0x60, 0x80)); + + if (io.MouseDown[0] && !isClicking && !isDraging && GImGui->ActiveId == 0) + { + overBox = boxCoordInt; + isClicking = true; + isDraging = true; + } + } + } + } + } + } + if (interpolationFrames) + { + interpolationFrames--; + vec_t newDir = viewInverse.v.dir; + newDir.Lerp(interpolationDir, 0.2f); + newDir.Normalize(); - if (isDraging) - { - matrix_t rx, ry, roll; + vec_t newUp = viewInverse.v.up; + newUp.Lerp(interpolationUp, 0.3f); + newUp.Normalize(); + newUp = interpolationUp; + vec_t newEye = camTarget + newDir * length; + LookAt(&newEye.x, &camTarget.x, &newUp.x, view); + } + gContext.mIsViewManipulatorHovered = gContext.mbMouseOver && ImRect(position, position + size).Contains(io.MousePos); + + if (io.MouseDown[0] && (fabsf(io.MouseDelta[0]) || fabsf(io.MouseDelta[1])) && isClicking) + { + isClicking = false; + } + + if (!io.MouseDown[0]) + { + if (isClicking) + { + // apply new view direction + int cx = overBox / 9; + int cy = (overBox - cx * 9) / 3; + int cz = overBox % 3; + interpolationDir = makeVect(1.f - (float)cx, 1.f - (float)cy, 1.f - (float)cz); + interpolationDir.Normalize(); + + if (fabsf(Dot(interpolationDir, referenceUp)) > 1.0f - 0.01f) + { + vec_t right = viewInverse.v.right; + if (fabsf(right.x) > fabsf(right.z)) + { + right.z = 0.f; + } + else + { + right.x = 0.f; + } + right.Normalize(); + interpolationUp = Cross(interpolationDir, right); + interpolationUp.Normalize(); + } + else + { + interpolationUp = referenceUp; + } + interpolationFrames = 40; + } + isClicking = false; + isDraging = false; + } - rx.RotationAxis(referenceUp, -io.MouseDelta.x * 0.01f); - ry.RotationAxis(viewInverse.v.right, -io.MouseDelta.y * 0.01f); + if (isDraging) + { + matrix_t rx, ry, roll; - roll = rx * ry; + rx.RotationAxis(referenceUp, -io.MouseDelta.x * 0.01f); + ry.RotationAxis(viewInverse.v.right, -io.MouseDelta.y * 0.01f); - vec_t newDir = viewInverse.v.dir; - newDir.TransformVector(roll); - newDir.Normalize(); + roll = rx * ry; - // clamp - vec_t planDir = Cross(viewInverse.v.right, referenceUp); - planDir.y = 0.f; - planDir.Normalize(); - float dt = Dot(planDir, newDir); - if (dt < 0.0f) - { - newDir += planDir * dt; + vec_t newDir = viewInverse.v.dir; + newDir.TransformVector(roll); newDir.Normalize(); - } - vec_t newEye = camTarget + newDir * length; - LookAt(&newEye.x, &camTarget.x, &referenceUp.x, view); - } + // clamp + vec_t planDir = Cross(viewInverse.v.right, referenceUp); + planDir.y = 0.f; + planDir.Normalize(); + float dt = Dot(planDir, newDir); + if (dt < 0.0f) + { + newDir += planDir * dt; + newDir.Normalize(); + } + + vec_t newEye = camTarget + newDir * length; + LookAt(&newEye.x, &camTarget.x, &referenceUp.x, view); + } - gContext.mbUsingViewManipulate = (interpolationFrames != 0) || isDraging; + gContext.mbUsingViewManipulate = (interpolationFrames != 0) || isDraging; - // restore view/projection because it was used to compute ray - ComputeContext(svgView.m16, svgProjection.m16, gContext.mModelSource.m16, gContext.mMode); - } -}; + // restore view/projection because it was used to compute ray + ComputeContext(svgView.m16, svgProjection.m16, gContext.mModelSource.m16, gContext.mMode); + } +}; // namespace IMGUIZMO_NAMESPACE diff --git a/Gems/ImGuizmo/Code/3rdParty/ImGuizmo/ImGuizmo.h b/Gems/ImGuizmo/Code/3rdParty/ImGuizmo/ImGuizmo.h index 56cc6dcc..98095d27 100644 --- a/Gems/ImGuizmo/Code/3rdParty/ImGuizmo/ImGuizmo.h +++ b/Gems/ImGuizmo/Code/3rdParty/ImGuizmo/ImGuizmo.h @@ -27,8 +27,8 @@ // History : // 2025/01/10 Adjustments in library during integration into O3DE // 2019/11/03 View gizmo -// 2016/09/11 Behind camera culling. Scaling Delta matrix not multiplied by source matrix scales. local/world rotation and translation fixed. Display message is incorrect (X: ... Y:...) in local mode. -// 2016/09/09 Hatched negative axis. Snapping. Documentation update. +// 2016/09/11 Behind camera culling. Scaling Delta matrix not multiplied by source matrix scales. local/world rotation and translation +// fixed. Display message is incorrect (X: ... Y:...) in local mode. 2016/09/09 Hatched negative axis. Snapping. Documentation update. // 2016/09/04 Axis switch and translation plan autohiding. Scale transform stability improved // 2016/09/01 Mogwai changed to Manipulate. Draw debug cube. Fixed inverted scale. Mixing scale and translation/rotation gives bad results. // 2016/08/31 First version @@ -121,154 +121,174 @@ struct ImGuiWindow; namespace IMGUIZMO_NAMESPACE { - // call inside your own window and before Manipulate() in order to draw gizmo to that window. - // Or pass a specific ImDrawList to draw to (e.g. ImGui::GetForegroundDrawList()). - IMGUI_API void SetDrawlist(ImDrawList* drawlist = nullptr); + // call inside your own window and before Manipulate() in order to draw gizmo to that window. + // Or pass a specific ImDrawList to draw to (e.g. ImGui::GetForegroundDrawList()). + IMGUI_API void SetDrawlist(ImDrawList* drawlist = nullptr); - // call BeginFrame right after ImGui_XXXX_NewFrame(); - IMGUI_API void BeginFrame(); + // call BeginFrame right after ImGui_XXXX_NewFrame(); + IMGUI_API void BeginFrame(); - // this is necessary because when imguizmo is compiled into a dll, and imgui into another - // globals are not shared between them. - // More details at https://stackoverflow.com/questions/19373061/what-happens-to-global-and-static-variables-in-a-shared-library-when-it-is-dynam - // expose method to set imgui context - IMGUI_API void SetImGuiContext(ImGuiContext* ctx); + // this is necessary because when imguizmo is compiled into a dll, and imgui into another + // globals are not shared between them. + // More details at + // https://stackoverflow.com/questions/19373061/what-happens-to-global-and-static-variables-in-a-shared-library-when-it-is-dynam expose + // method to set imgui context + IMGUI_API void SetImGuiContext(ImGuiContext* ctx); - // return true if mouse cursor is over any gizmo control (axis, plan or screen component) - IMGUI_API bool IsOver(); + // return true if mouse cursor is over any gizmo control (axis, plan or screen component) + IMGUI_API bool IsOver(); - // return true if mouse IsOver or if the gizmo is in moving state - IMGUI_API bool IsUsing(); + // return true if mouse IsOver or if the gizmo is in moving state + IMGUI_API bool IsUsing(); - // return true if the view gizmo is in moving state - IMGUI_API bool IsUsingViewManipulate(); - // only check if your mouse is over the view manipulator - no matter whether it's active or not - IMGUI_API bool IsViewManipulateHovered(); + // return true if the view gizmo is in moving state + IMGUI_API bool IsUsingViewManipulate(); + // only check if your mouse is over the view manipulator - no matter whether it's active or not + IMGUI_API bool IsViewManipulateHovered(); - // return true if any gizmo is in moving state - IMGUI_API bool IsUsingAny(); + // return true if any gizmo is in moving state + IMGUI_API bool IsUsingAny(); - // enable/disable the gizmo. Stay in the state until next call to Enable. - // gizmo is rendered with gray half transparent color when disabled - IMGUI_API void Enable(bool enable); + // enable/disable the gizmo. Stay in the state until next call to Enable. + // gizmo is rendered with gray half transparent color when disabled + IMGUI_API void Enable(bool enable); - // helper functions for manualy editing translation/rotation/scale with an input float - // translation, rotation and scale float points to 3 floats each - // Angles are in degrees (more suitable for human editing) - // example: - // float matrixTranslation[3], matrixRotation[3], matrixScale[3]; - // ImGuizmo::DecomposeMatrixToComponents(gizmoMatrix.m16, matrixTranslation, matrixRotation, matrixScale); - // ImGui::InputFloat3("Tr", matrixTranslation, 3); - // ImGui::InputFloat3("Rt", matrixRotation, 3); - // ImGui::InputFloat3("Sc", matrixScale, 3); - // ImGuizmo::RecomposeMatrixFromComponents(matrixTranslation, matrixRotation, matrixScale, gizmoMatrix.m16); - // - // These functions have some numerical stability issues for now. Use with caution. - IMGUI_API void DecomposeMatrixToComponents(const float* matrix, float* translation, float* rotation, float* scale); - IMGUI_API void RecomposeMatrixFromComponents(const float* translation, const float* rotation, const float* scale, float* matrix); + // helper functions for manualy editing translation/rotation/scale with an input float + // translation, rotation and scale float points to 3 floats each + // Angles are in degrees (more suitable for human editing) + // example: + // float matrixTranslation[3], matrixRotation[3], matrixScale[3]; + // ImGuizmo::DecomposeMatrixToComponents(gizmoMatrix.m16, matrixTranslation, matrixRotation, matrixScale); + // ImGui::InputFloat3("Tr", matrixTranslation, 3); + // ImGui::InputFloat3("Rt", matrixRotation, 3); + // ImGui::InputFloat3("Sc", matrixScale, 3); + // ImGuizmo::RecomposeMatrixFromComponents(matrixTranslation, matrixRotation, matrixScale, gizmoMatrix.m16); + // + // These functions have some numerical stability issues for now. Use with caution. + IMGUI_API void DecomposeMatrixToComponents(const float* matrix, float* translation, float* rotation, float* scale); + IMGUI_API void RecomposeMatrixFromComponents(const float* translation, const float* rotation, const float* scale, float* matrix); - IMGUI_API void SetRect(float x, float y, float width, float height); - // default is false - IMGUI_API void SetOrthographic(bool isOrthographic); + IMGUI_API void SetRect(float x, float y, float width, float height); + // default is false + IMGUI_API void SetOrthographic(bool isOrthographic); - // Render a cube with face color corresponding to face normal. Usefull for debug/tests - IMGUI_API void DrawCubes(const float* view, const float* projection, const float* matrices, int matrixCount); - IMGUI_API void DrawGrid(const float* view, const float* projection, const float* matrix, const float gridSize); + // Render a cube with face color corresponding to face normal. Usefull for debug/tests + IMGUI_API void DrawCubes(const float* view, const float* projection, const float* matrices, int matrixCount); + IMGUI_API void DrawGrid(const float* view, const float* projection, const float* matrix, const float gridSize); - // call it when you want a gizmo - // Needs view and projection matrices. - // matrix parameter is the source matrix (where will be gizmo be drawn) and might be transformed by the function. Return deltaMatrix is optional - // translation is applied in world space + // call it when you want a gizmo + // Needs view and projection matrices. + // matrix parameter is the source matrix (where will be gizmo be drawn) and might be transformed by the function. Return deltaMatrix is + // optional translation is applied in world space - IMGUI_API bool Manipulate(const float* view, const float* projection, OPERATION operation, MODE mode, float* matrix, float* deltaMatrix = NULL, const float* snap = NULL, const float* localBounds = NULL, const float* boundsSnap = NULL); - // - // Please note that this cubeview is patented by Autodesk : https://patents.google.com/patent/US7782319B2/en - // It seems to be a defensive patent in the US. I don't think it will bring troubles using it as - // other software are using the same mechanics. But just in case, you are now warned! - // - IMGUI_API void ViewManipulate(float* view, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor); + IMGUI_API bool Manipulate( + const float* view, + const float* projection, + OPERATION operation, + MODE mode, + float* matrix, + float* deltaMatrix = NULL, + const float* snap = NULL, + const float* localBounds = NULL, + const float* boundsSnap = NULL); + // + // Please note that this cubeview is patented by Autodesk : https://patents.google.com/patent/US7782319B2/en + // It seems to be a defensive patent in the US. I don't think it will bring troubles using it as + // other software are using the same mechanics. But just in case, you are now warned! + // + IMGUI_API void ViewManipulate(float* view, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor); - // use this version if you did not call Manipulate before and you are just using ViewManipulate - IMGUI_API void ViewManipulate(float* view, const float* projection, OPERATION operation, MODE mode, float* matrix, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor); + // use this version if you did not call Manipulate before and you are just using ViewManipulate + IMGUI_API void ViewManipulate( + float* view, + const float* projection, + OPERATION operation, + MODE mode, + float* matrix, + float length, + ImVec2 position, + ImVec2 size, + ImU32 backgroundColor); - IMGUI_API void SetAlternativeWindow(ImGuiWindow* window); + IMGUI_API void SetAlternativeWindow(ImGuiWindow* window); - [[deprecated("Use PushID/PopID instead.")]] - IMGUI_API void SetID(int id); + [[deprecated("Use PushID/PopID instead.")]] IMGUI_API void SetID(int id); - // ID stack/scopes - // Read the FAQ (docs/FAQ.md or http://dearimgui.org/faq) for more details about how ID are handled in dear imgui. - // - Those questions are answered and impacted by understanding of the ID stack system: - // - "Q: Why is my widget not reacting when I click on it?" - // - "Q: How can I have widgets with an empty label?" - // - "Q: How can I have multiple widgets with the same label?" - // - Short version: ID are hashes of the entire ID stack. If you are creating widgets in a loop you most likely - // want to push a unique identifier (e.g. object pointer, loop index) to uniquely differentiate them. - // - You can also use the "Label##foobar" syntax within widget label to distinguish them from each others. - // - In this header file we use the "label"/"name" terminology to denote a string that will be displayed + used as an ID, - // whereas "str_id" denote a string that is only used as an ID and not normally displayed. - IMGUI_API void PushID(const char* str_id); // push string into the ID stack (will hash string). - IMGUI_API void PushID(const char* str_id_begin, const char* str_id_end); // push string into the ID stack (will hash string). - IMGUI_API void PushID(const void* ptr_id); // push pointer into the ID stack (will hash pointer). - IMGUI_API void PushID(int int_id); // push integer into the ID stack (will hash integer). - IMGUI_API void PopID(); // pop from the ID stack. - IMGUI_API ImGuiID GetID(const char* str_id); // calculate unique ID (hash of whole ID stack + given parameter). e.g. if you want to query into ImGuiStorage yourself - IMGUI_API ImGuiID GetID(const char* str_id_begin, const char* str_id_end); - IMGUI_API ImGuiID GetID(const void* ptr_id); + // ID stack/scopes + // Read the FAQ (docs/FAQ.md or http://dearimgui.org/faq) for more details about how ID are handled in dear imgui. + // - Those questions are answered and impacted by understanding of the ID stack system: + // - "Q: Why is my widget not reacting when I click on it?" + // - "Q: How can I have widgets with an empty label?" + // - "Q: How can I have multiple widgets with the same label?" + // - Short version: ID are hashes of the entire ID stack. If you are creating widgets in a loop you most likely + // want to push a unique identifier (e.g. object pointer, loop index) to uniquely differentiate them. + // - You can also use the "Label##foobar" syntax within widget label to distinguish them from each others. + // - In this header file we use the "label"/"name" terminology to denote a string that will be displayed + used as an ID, + // whereas "str_id" denote a string that is only used as an ID and not normally displayed. + IMGUI_API void PushID(const char* str_id); // push string into the ID stack (will hash string). + IMGUI_API void PushID(const char* str_id_begin, const char* str_id_end); // push string into the ID stack (will hash string). + IMGUI_API void PushID(const void* ptr_id); // push pointer into the ID stack (will hash pointer). + IMGUI_API void PushID(int int_id); // push integer into the ID stack (will hash integer). + IMGUI_API void PopID(); // pop from the ID stack. + IMGUI_API ImGuiID GetID(const char* str_id); // calculate unique ID (hash of whole ID stack + given parameter). e.g. if you want to + // query into ImGuiStorage yourself + IMGUI_API ImGuiID GetID(const char* str_id_begin, const char* str_id_end); + IMGUI_API ImGuiID GetID(const void* ptr_id); - // return true if the cursor is over the operation's gizmo - IMGUI_API bool IsOver(OPERATION op); - IMGUI_API void SetGizmoSizeClipSpace(float value); + // return true if the cursor is over the operation's gizmo + IMGUI_API bool IsOver(OPERATION op); + IMGUI_API void SetGizmoSizeClipSpace(float value); - // Allow axis to flip - // When true (default), the guizmo axis flip for better visibility - // When false, they always stay along the positive world/local axis - IMGUI_API void AllowAxisFlip(bool value); + // Allow axis to flip + // When true (default), the guizmo axis flip for better visibility + // When false, they always stay along the positive world/local axis + IMGUI_API void AllowAxisFlip(bool value); - // Configure the limit where axis are hidden - IMGUI_API void SetAxisLimit(float value); - // Set an axis mask to permanently hide a given axis (true -> hidden, false -> shown) - IMGUI_API void SetAxisMask(bool x, bool y, bool z); - // Configure the limit where planes are hiden - IMGUI_API void SetPlaneLimit(float value); - // from a x,y,z point in space and using Manipulation view/projection matrix, check if mouse is in pixel radius distance of that projected point - IMGUI_API bool IsOver(float* position, float pixelRadius); + // Configure the limit where axis are hidden + IMGUI_API void SetAxisLimit(float value); + // Set an axis mask to permanently hide a given axis (true -> hidden, false -> shown) + IMGUI_API void SetAxisMask(bool x, bool y, bool z); + // Configure the limit where planes are hiden + IMGUI_API void SetPlaneLimit(float value); + // from a x,y,z point in space and using Manipulation view/projection matrix, check if mouse is in pixel radius distance of that + // projected point + IMGUI_API bool IsOver(float* position, float pixelRadius); - enum COLOR - { - DIRECTION_X, // directionColor[0] - DIRECTION_Y, // directionColor[1] - DIRECTION_Z, // directionColor[2] - PLANE_X, // planeColor[0] - PLANE_Y, // planeColor[1] - PLANE_Z, // planeColor[2] - SELECTION, // selectionColor - INACTIVE, // inactiveColor - TRANSLATION_LINE, // translationLineColor - SCALE_LINE, - ROTATION_USING_BORDER, - ROTATION_USING_FILL, - HATCHED_AXIS_LINES, - TEXT, - TEXT_SHADOW, - COUNT - }; + enum COLOR + { + DIRECTION_X, // directionColor[0] + DIRECTION_Y, // directionColor[1] + DIRECTION_Z, // directionColor[2] + PLANE_X, // planeColor[0] + PLANE_Y, // planeColor[1] + PLANE_Z, // planeColor[2] + SELECTION, // selectionColor + INACTIVE, // inactiveColor + TRANSLATION_LINE, // translationLineColor + SCALE_LINE, + ROTATION_USING_BORDER, + ROTATION_USING_FILL, + HATCHED_AXIS_LINES, + TEXT, + TEXT_SHADOW, + COUNT + }; - struct Style - { - IMGUI_API Style(); + struct Style + { + IMGUI_API Style(); - float TranslationLineThickness; // Thickness of lines for translation gizmo - float TranslationLineArrowSize; // Size of arrow at the end of lines for translation gizmo - float RotationLineThickness; // Thickness of lines for rotation gizmo - float RotationOuterLineThickness; // Thickness of line surrounding the rotation gizmo - float ScaleLineThickness; // Thickness of lines for scale gizmo - float ScaleLineCircleSize; // Size of circle at the end of lines for scale gizmo - float HatchedAxisLineThickness; // Thickness of hatched axis lines - float CenterCircleSize; // Size of circle at the center of the translate/scale gizmo + float TranslationLineThickness; // Thickness of lines for translation gizmo + float TranslationLineArrowSize; // Size of arrow at the end of lines for translation gizmo + float RotationLineThickness; // Thickness of lines for rotation gizmo + float RotationOuterLineThickness; // Thickness of line surrounding the rotation gizmo + float ScaleLineThickness; // Thickness of lines for scale gizmo + float ScaleLineCircleSize; // Size of circle at the end of lines for scale gizmo + float HatchedAxisLineThickness; // Thickness of hatched axis lines + float CenterCircleSize; // Size of circle at the center of the translate/scale gizmo - ImVec4 Colors[COLOR::COUNT]; - }; + ImVec4 Colors[COLOR::COUNT]; + }; - IMGUI_API Style& GetStyle(); -} + IMGUI_API Style& GetStyle(); +} // namespace IMGUIZMO_NAMESPACE From db6c49b7f901fadcee0cf4764e7cf802b9b8e95a Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 16:01:58 +0100 Subject: [PATCH 079/175] add biref summary for profiler bus Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 123 ++++++++++++++++-- .../Clients/FPSProfilerSystemComponent.cpp | 21 +-- .../Clients/FPSProfilerSystemComponent.h | 4 +- 3 files changed, 127 insertions(+), 21 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 7dabbd0c..2944d4d4 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -15,30 +15,104 @@ namespace FPSProfiler AZ_RTTI(FPSProfilerRequests, FPSProfilerRequestsTypeId); virtual ~FPSProfilerRequests() = default; - // Profiler control + /** + * @brief Starts the profiling process to track system performance. + * + * Calls @ResetProfilingData and creates a csv file from provided path. + * @attention You can change path with @ChangeSavePath or @SafeChangeSavePath. + */ virtual void StartProfiling() = 0; + + /** + * @brief Stops the profiling process and saves data collection to provided path. + * @attention You can change path with @ChangeSavePath or @SafeChangeSavePath. + */ virtual void StopProfiling() = 0; + + /** + * @brief Resets all collected profiling data, clearing previous statistics. + * @attention Reset is not calling @SaveLogToFile, it needs to be called manually. + */ virtual void ResetProfilingData() = 0; + + /** + * @brief Checks whether profiling is currently active. + * @return True if profiling is enabled, false otherwise. + */ [[nodiscard]] virtual bool IsProfiling() const = 0; + + /** + * @brief Checks if any save option for profiling data is enabled. + * @return True if at least one save option is active, false otherwise. + */ [[nodiscard]] virtual bool IsAnySaveOptionEnabled() const = 0; - virtual void ChangeSavePath( - const AZ::IO::Path& newSavePath) = 0; //!< Caution! This function is not runtime safe. Instead, use @ref SafeChangeSavePath - virtual void SafeChangeSavePath(const AZ::IO::Path& newSavePath) = 0; //!< Runtime safe path changing. Saves and stops current - //!< profiling and changes path afterward. - // Get Fps Data + /** + * @brief Changes the save path for profiling data. + * @warning This function is NOT runtime safe. Use @ref SafeChangeSavePath instead. + * @param newSavePath The new file path where profiling data should be saved. + */ + virtual void ChangeSavePath(const AZ::IO::Path& newSavePath) = 0; + + /** + * @brief Safely changes the save path during runtime. + * This method stops profiling, saves the current data, and then updates the path. + * @param newSavePath The new file path where profiling data should be saved. + */ + virtual void SafeChangeSavePath(const AZ::IO::Path& newSavePath) = 0; + + /** + * @brief Retrieves the minimum recorded FPS during the profiling session. + * @return The lowest FPS value recorded at the moment of the call. + */ [[nodiscard]] virtual float GetMinFps() const = 0; + + /** + * @brief Retrieves the maximum recorded FPS during the profiling session. + * @return The highest FPS value recorded at the moment of the call. + */ [[nodiscard]] virtual float GetMaxFps() const = 0; + + /** + * @brief Retrieves the average FPS over the profiling session. + * @return The average FPS value at the moment of the call. + */ [[nodiscard]] virtual float GetAvgFps() const = 0; + + /** + * @brief Retrieves the current real-time FPS value. + * @return The FPS value at the moment of the call. + */ [[nodiscard]] virtual float GetCurrentFps() const = 0; - // Memory usage - [[nodiscard]] virtual size_t GetCpuMemoryUsed() const = 0; - [[nodiscard]] virtual size_t GetGpuMemoryUsed() const = 0; + /** + * @brief Gets the current CPU memory usage. + * @return A pair containing the currently used and reserved CPU memory (in bytes). + */ + [[nodiscard]] virtual AZStd::pair GetCpuMemoryUsed() const = 0; - // Logging + /** + * @brief Gets the current GPU memory usage. + * @return A pair containing the currently used and reserved GPU memory (in bytes). + */ + [[nodiscard]] virtual AZStd::pair GetGpuMemoryUsed() const = 0; + + /** + * @brief Saves the current profiling log to a csv file at the predefined save path. + */ virtual void SaveLogToFile() = 0; + + /** + * @brief Saves the profiling log to a specified file path. + * @param newSavePath The csv file path where the log should be saved. + * @param useSafeChangePath If true, the function will use @ref SafeChangeSavePath to ensure runtime safety. + */ virtual void SaveLogToFileWithNewPath(const AZ::IO::Path& newSavePath, bool useSafeChangePath) = 0; + + /** + * @brief Enables or disables FPS display on-screen. + * @param enable If true, FPS will be displayed; otherwise, it will be hidden. + */ virtual void ShowFpsOnScreen(bool enable) = 0; }; @@ -62,21 +136,50 @@ namespace FPSProfiler AZ_RTTI(FPSProfilerNotifications, FPSProfilerNotificationsTypeId); virtual ~FPSProfilerNotifications() = default; + /** + * @brief Called when a new file is created. + * @param filePath The path of the newly created file. + */ virtual void OnFileCreated(const AZ::IO::Path& filePath) { } + + /** + * @brief Called when an existing file is updated. + * @param filePath The path of the file that was modified. + */ virtual void OnFileUpdate(const AZ::IO::Path& filePath) { } + + /** + * @brief Called when a file is successfully saved. + * @param filePath The path of the saved file. + */ virtual void OnFileSaved(const AZ::IO::Path& filePath) { } + + /** + * @brief Called when the profiling process starts. + * @param config The configuration settings used for the profiling session. + */ virtual void OnProfileStart(const FPSProfilerConfig& config) { } + + /** + * @brief Called when the profiling data is reset. + * @param config The configuration settings used for the profiling session. + */ virtual void OnProfileReset(const FPSProfilerConfig& config) { } + + /** + * @brief Called when the profiling process stops. + * @param config The configuration settings used for the profiling session. + */ virtual void OnProfileStop(const FPSProfilerConfig& config) { } diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 30bd713b..4fdaa090 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -91,6 +91,8 @@ namespace FPSProfiler void FPSProfilerSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { + AZ_PROFILE_SCOPE(AzCore, "FPSProfiler::OnTick()"); + if (!m_isProfiling) { return; @@ -123,8 +125,8 @@ namespace FPSProfiler m_minFps, m_maxFps, m_avgFps, - m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed()) : -1.0f, - m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed()) : -1.0f); + m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed().first) : -1.0f, + m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed().first) : -1.0f); } else { @@ -132,8 +134,8 @@ namespace FPSProfiler logEntry, LineSize, "-1,-1.0,-1.0,-1.0,-1.0,-1.0,%.2f,%.2f\n", - m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed()) : -1.0f, - m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed()) : -1.0f); + m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed().first) : -1.0f, + m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed().first) : -1.0f); } m_logBuffer.insert(m_logBuffer.end(), logEntry, logEntry + logEntryLength); @@ -254,16 +256,16 @@ namespace FPSProfiler return m_currentFps; } - size_t FPSProfilerSystemComponent::GetCpuMemoryUsed() const + AZStd::pair FPSProfilerSystemComponent::GetCpuMemoryUsed() const { size_t usedBytes = 0; size_t reservedBytes = 0; AZ::AllocatorManager::Instance().GetAllocatorStats(usedBytes, reservedBytes); - return usedBytes; + return { usedBytes, reservedBytes }; } - size_t FPSProfilerSystemComponent::GetGpuMemoryUsed() const + AZStd::pair FPSProfilerSystemComponent::GetGpuMemoryUsed() const { if (AZ::RHI::RHISystemInterface* rhiSystem = AZ::RHI::RHISystemInterface::Get()) { @@ -272,11 +274,12 @@ namespace FPSProfiler AZ::RHI::MemoryStatistics memoryStats; device->CompileMemoryStatistics(memoryStats, AZ::RHI::MemoryStatisticsReportFlags::Basic); - return memoryStats.m_heaps.front().m_memoryUsage.m_totalResidentInBytes; + return { memoryStats.m_heaps.front().m_memoryUsage.m_totalResidentInBytes, + memoryStats.m_heaps.front().m_memoryUsage.m_budgetInBytes }; } } - return 0; + return { 0, 0 }; } void FPSProfilerSystemComponent::SaveLogToFile() diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index ddec0a78..33893660 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -47,8 +47,8 @@ namespace FPSProfiler [[nodiscard]] float GetMaxFps() const override; [[nodiscard]] float GetAvgFps() const override; [[nodiscard]] float GetCurrentFps() const override; - [[nodiscard]] size_t GetCpuMemoryUsed() const override; - [[nodiscard]] size_t GetGpuMemoryUsed() const override; + [[nodiscard]] AZStd::pair GetCpuMemoryUsed() const override; + [[nodiscard]] AZStd::pair GetGpuMemoryUsed() const override; void SaveLogToFile() override; void SaveLogToFileWithNewPath(const AZ::IO::Path& newSavePath, bool useSafeChangePath) override; void ShowFpsOnScreen(bool enable) override; From cc6f38e6b679bfab39b62c5054f22b9db17e1dcd Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 16:12:25 +0100 Subject: [PATCH 080/175] add to log buffer reserved memory data Signed-off-by: Wojciech Czerski --- .../Source/Clients/FPSProfilerSystemComponent.cpp | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 4fdaa090..7b816bc6 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -118,7 +118,7 @@ namespace FPSProfiler logEntryLength = azsnprintf( logEntry, LineSize, - "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n", + "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n", m_frameCount, deltaTime, m_currentFps, @@ -126,16 +126,20 @@ namespace FPSProfiler m_maxFps, m_avgFps, m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed().first) : -1.0f, - m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed().first) : -1.0f); + m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed().second) : -1.0f, + m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed().first) : -1.0f, + m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed().second) : -1.0f); } else { logEntryLength = azsnprintf( logEntry, LineSize, - "-1,-1.0,-1.0,-1.0,-1.0,-1.0,%.2f,%.2f\n", + "-1,-1.0,-1.0,-1.0,-1.0,-1.0,%.2f,%.2f,%.2f,%.2f\n", m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed().first) : -1.0f, - m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed().first) : -1.0f); + m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed().second) : -1.0f, + m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed().first) : -1.0f, + m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed().second) : -1.0f); } m_logBuffer.insert(m_logBuffer.end(), logEntry, logEntry + logEntryLength); @@ -358,7 +362,8 @@ namespace FPSProfiler // Write profiling headers to file AZ::IO::FileIOStream file(m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath); - AZStd::string csvHeader = "Frame,FrameTime,CurrentFPS,MinFPS,MaxFPS,AvgFPS,CpuMemoryUsed,GpuMemoryUsed\n"; + AZStd::string csvHeader = + "Frame,FrameTime,CurrentFPS,MinFPS,MaxFPS,AvgFPS,CpuMemoryUsed,CpuMemoryReserved,GpuMemoryUsed,GpuMemoryReserved\n"; file.Write(csvHeader.size(), csvHeader.c_str()); file.Close(); From 4d8ca576e3581ece8750e102a7fcbb0ea85f9086 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 16:15:52 +0100 Subject: [PATCH 081/175] change sizte_to -> AZStd::size_t Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 4 ++-- .../Source/Clients/FPSProfilerSystemComponent.cpp | 11 +++++------ .../Code/Source/Clients/FPSProfilerSystemComponent.h | 7 ++++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 2944d4d4..bb863fd3 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -89,13 +89,13 @@ namespace FPSProfiler * @brief Gets the current CPU memory usage. * @return A pair containing the currently used and reserved CPU memory (in bytes). */ - [[nodiscard]] virtual AZStd::pair GetCpuMemoryUsed() const = 0; + [[nodiscard]] virtual AZStd::pair GetCpuMemoryUsed() const = 0; /** * @brief Gets the current GPU memory usage. * @return A pair containing the currently used and reserved GPU memory (in bytes). */ - [[nodiscard]] virtual AZStd::pair GetGpuMemoryUsed() const = 0; + [[nodiscard]] virtual AZStd::pair GetGpuMemoryUsed() const = 0; /** * @brief Saves the current profiling log to a csv file at the predefined save path. diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 7b816bc6..1991e838 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -109,7 +109,6 @@ namespace FPSProfiler return; } - constexpr size_t LineSize = 128; char logEntry[LineSize]; int logEntryLength = 0; @@ -260,16 +259,16 @@ namespace FPSProfiler return m_currentFps; } - AZStd::pair FPSProfilerSystemComponent::GetCpuMemoryUsed() const + AZStd::pair FPSProfilerSystemComponent::GetCpuMemoryUsed() const { - size_t usedBytes = 0; - size_t reservedBytes = 0; + AZStd::size_t usedBytes = 0; + AZStd::size_t reservedBytes = 0; AZ::AllocatorManager::Instance().GetAllocatorStats(usedBytes, reservedBytes); return { usedBytes, reservedBytes }; } - AZStd::pair FPSProfilerSystemComponent::GetGpuMemoryUsed() const + AZStd::pair FPSProfilerSystemComponent::GetGpuMemoryUsed() const { if (AZ::RHI::RHISystemInterface* rhiSystem = AZ::RHI::RHISystemInterface::Get()) { @@ -395,7 +394,7 @@ namespace FPSProfiler FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileUpdate, m_configuration.m_OutputFilename.c_str()); } - float FPSProfilerSystemComponent::BytesToMB(size_t bytes) + float FPSProfilerSystemComponent::BytesToMB(AZStd::size_t bytes) { return static_cast(bytes) / (1024.0f * 1024.0f); } diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 33893660..c91f91ad 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -47,8 +47,8 @@ namespace FPSProfiler [[nodiscard]] float GetMaxFps() const override; [[nodiscard]] float GetAvgFps() const override; [[nodiscard]] float GetCurrentFps() const override; - [[nodiscard]] AZStd::pair GetCpuMemoryUsed() const override; - [[nodiscard]] AZStd::pair GetGpuMemoryUsed() const override; + [[nodiscard]] AZStd::pair GetCpuMemoryUsed() const override; + [[nodiscard]] AZStd::pair GetGpuMemoryUsed() const override; void SaveLogToFile() override; void SaveLogToFileWithNewPath(const AZ::IO::Path& newSavePath, bool useSafeChangePath) override; void ShowFpsOnScreen(bool enable) override; @@ -69,6 +69,7 @@ namespace FPSProfiler AZStd::vector m_logBuffer; // Vector of collected log entries. Cleared after @ref // m_configuration.m_AutoSaveAtFrame, when @ref m_configuration.m_AutoSave enabled. static constexpr AZStd::size_t MAX_LOG_BUFFER_SIZE = 1024 * 128; // Max buffer size for @ref m_requiredLogBufferSize. + static constexpr AZStd::size_t LineSize = 128; // Max line length // File operations void CreateLogFile(); @@ -76,7 +77,7 @@ namespace FPSProfiler // Helpers void CalculateFpsData(const float& deltaTime); - static float BytesToMB(size_t bytes); + static float BytesToMB(AZStd::size_t bytes); static bool IsPathValid(const AZ::IO::Path& path); // Debug display From f033838807305e8899951af3b5e973d6c7e251ab Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 17:03:31 +0100 Subject: [PATCH 082/175] fix layout | move buffer size to constexpr outside scope Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 48 ++++++++++++++----- .../Clients/FPSProfilerSystemComponent.h | 2 +- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 1991e838..4a7b4b5c 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -97,6 +97,8 @@ namespace FPSProfiler { return; } + + // Update FPS data CalculateFpsData(deltaTime); if (m_configuration.m_ShowFps) @@ -109,14 +111,34 @@ namespace FPSProfiler return; } - char logEntry[LineSize]; + // Initialize log entry buffer + char logEntry[MAX_LOG_BUFFER_LINE_SIZE]; int logEntryLength = 0; + // Initialize memory usage values + float usedCpu = -1.0f, reservedCpu = -1.0f; + float usedGpu = -1.0f, reservedGpu = -1.0f; + + if (m_configuration.m_SaveCpuData) + { + auto [cpuUsed, cpuReserved] = GetCpuMemoryUsed(); + usedCpu = BytesToMB(cpuUsed); + reservedCpu = BytesToMB(cpuReserved); + } + + if (m_configuration.m_SaveGpuData) + { + auto [gpuUsed, gpuReserved] = GetGpuMemoryUsed(); + usedGpu = BytesToMB(gpuUsed); + reservedGpu = BytesToMB(gpuReserved); + } + + // Format log entry if (m_configuration.m_SaveFpsData) { logEntryLength = azsnprintf( logEntry, - LineSize, + MAX_LOG_BUFFER_LINE_SIZE, "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n", m_frameCount, deltaTime, @@ -124,26 +146,26 @@ namespace FPSProfiler m_minFps, m_maxFps, m_avgFps, - m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed().first) : -1.0f, - m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed().second) : -1.0f, - m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed().first) : -1.0f, - m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed().second) : -1.0f); + usedCpu, + reservedCpu, + usedGpu, + reservedGpu); } else { logEntryLength = azsnprintf( logEntry, - LineSize, + MAX_LOG_BUFFER_LINE_SIZE, "-1,-1.0,-1.0,-1.0,-1.0,-1.0,%.2f,%.2f,%.2f,%.2f\n", - m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed().first) : -1.0f, - m_configuration.m_SaveCpuData ? BytesToMB(GetCpuMemoryUsed().second) : -1.0f, - m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed().first) : -1.0f, - m_configuration.m_SaveGpuData ? BytesToMB(GetGpuMemoryUsed().second) : -1.0f); + usedCpu, + reservedCpu, + usedGpu, + reservedGpu); } m_logBuffer.insert(m_logBuffer.end(), logEntry, logEntry + logEntryLength); - // Auto Save - if (m_configuration.m_AutoSave && m_frameCount % m_configuration.m_AutoSaveAtFrame == 0) + // Auto save + if (m_configuration.m_AutoSave && (m_frameCount % m_configuration.m_AutoSaveAtFrame == 0)) { WriteDataToFile(); } diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index c91f91ad..1a091629 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -69,7 +69,7 @@ namespace FPSProfiler AZStd::vector m_logBuffer; // Vector of collected log entries. Cleared after @ref // m_configuration.m_AutoSaveAtFrame, when @ref m_configuration.m_AutoSave enabled. static constexpr AZStd::size_t MAX_LOG_BUFFER_SIZE = 1024 * 128; // Max buffer size for @ref m_requiredLogBufferSize. - static constexpr AZStd::size_t LineSize = 128; // Max line length + static constexpr AZStd::size_t MAX_LOG_BUFFER_LINE_SIZE = 128; // Max line length // File operations void CreateLogFile(); From 26eadbb5b0f1926f73c6ca1e7fef212df8e734a5 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 17:10:33 +0100 Subject: [PATCH 083/175] fix table in readme | imrpove readability Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.h | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 1a091629..749961d0 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -54,33 +54,37 @@ namespace FPSProfiler void ShowFpsOnScreen(bool enable) override; private: - // Profiler Configuration - Editor Settings - FPSProfilerConfig m_configuration; - - // Profiler Data - bool m_isProfiling = false; - float m_minFps = 0.0f; // Tracking the lowest FPS value - float m_maxFps = 0.0f; // Tracking the highest FPS value - float m_avgFps = 0.0f; // Mean Value of accumulated current FPS - float m_currentFps = 0.0f; // Actual FPS in current frame - float m_totalFrameTime = 0.0f; // Time it took to enter frame - int m_frameCount = 0; // Numeric value of actual frame - AZStd::deque m_fpsSamples; // Deque of collected current FPSs. Used for calculating @ref m_avgFps. - AZStd::vector m_logBuffer; // Vector of collected log entries. Cleared after @ref - // m_configuration.m_AutoSaveAtFrame, when @ref m_configuration.m_AutoSave enabled. - static constexpr AZStd::size_t MAX_LOG_BUFFER_SIZE = 1024 * 128; // Max buffer size for @ref m_requiredLogBufferSize. - static constexpr AZStd::size_t MAX_LOG_BUFFER_LINE_SIZE = 128; // Max line length - - // File operations + // Profiler Configuration + FPSProfilerConfig m_configuration; //!< Stores editor settings for the profiler + + // Profiling State + bool m_isProfiling = false; //!< Flag to indicate if profiling is active + + // FPS Tracking Data + float m_minFps = 0.0f; //!< Lowest FPS value recorded + float m_maxFps = 0.0f; //!< Highest FPS value recorded + float m_avgFps = 0.0f; //!< Mean value of collected FPS samples + float m_currentFps = 0.0f; //!< FPS in the current frame + float m_totalFrameTime = 0.0f; //!< Time taken for the current frame + int m_frameCount = 0; //!< Total number of frames processed + + AZStd::deque m_fpsSamples; //!< Stores recent FPS values for averaging + + // Log Buffer + AZStd::vector m_logBuffer; //!< Buffer for log entries, cleared periodically if auto save enabled. + static constexpr AZStd::size_t MAX_LOG_BUFFER_SIZE = 1024 * 128; //!< Max log buffer size + static constexpr AZStd::size_t MAX_LOG_BUFFER_LINE_SIZE = 128; //!< Max length per log line + + // File Operations void CreateLogFile(); void WriteDataToFile(); - // Helpers + // Utility Functions void CalculateFpsData(const float& deltaTime); static float BytesToMB(AZStd::size_t bytes); static bool IsPathValid(const AZ::IO::Path& path); - // Debug display + // Debug Display void ShowFps() const; }; } // namespace FPSProfiler From 4f4146d02001bc5463c73bdcbe07ae168d54d67e Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 4 Mar 2025 17:11:11 +0100 Subject: [PATCH 084/175] fix readme table Signed-off-by: Wojciech Czerski --- readme.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/readme.md b/readme.md index 97980663..24d652ac 100644 --- a/readme.md +++ b/readme.md @@ -395,12 +395,11 @@ FPSProfilerRequestBus::BroadcastResult(currentFps, &FPSProfilerRequests::GetCurr ``` ## CSV File Example -| Frame | FrameTime | CurrentFPS | MinFPS | MaxFPS | AvgFPS | CpuMemoryUsed | GpuMemoryUsed | -|-------|--------------|--------------|-----------|-----------|-----------|------------------|-----------------| -| 1 | 5347.2783 | 0.00 | MAX_FLOAT | 0.00 | 0.00 | 1349.42 | 1752.12 | -| 2 | 0.4207 | 2.38 | 2.38 | 2.38 | 1.19 | 1375.50 | 2999.38 | -| 3 | 0.1934 | 5.17 | 2.38 | 5.17 | 2.52 | 1400.49 | 2963.44 | -| ... | ... | ... | ... | ... | ... | ... | ... | -| 100 | 0.0728 | 13.74 | 0.16 | 24.31 | 20.76 | 824.42 | 2553.17 | -| 101 | 0.1066 | 9.38 | 0.16 | 24.31 | 9.38 | 824.43 | 2595.25 | -| 102 | 0.0556 | 17.99 | 0.16 | 24.31 | 13.69 | 827.33 | 2595.25 | +| Frame | FrameTime | CurrentFPS | MinFPS | MaxFPS | AvgFPS | CpuMemoryUsed | CpuMemoryReserved | GpuMemoryUsed | GpuMemoryReserved | +|-------|-----------|------------|--------|--------|--------|---------------|-------------------|---------------|--------------------| +| 1 | 0.3943 | 2.54 | 2.54 | 2.54 | 2.54 | 2166.44 | 237568 | 3756.19 | 6930.19 | +| 2 | 0.1643 | 6.09 | 2.54 | 6.09 | 4.31 | 2182.99 | 237568 | 4126.19 | 6928.12 | +| 3 | 0.1150 | 8.69 | 2.54 | 8.69 | 5.77 | 2183.49 | 237568 | 3134.19 | 6928.69 | +| 4 | 0.0203 | 49.33 | 2.54 | 49.33 | 16.66 | 2181.58 | 237568 | 2654.19 | 6928.69 | +| 5 | 0.0282 | 35.46 | 2.54 | 49.33 | 20.42 | 2181.20 | 237568 | 2654.19 | 6928.69 | + From 64aca4416903cee284819baed560df0bd33084a0 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 09:47:04 +0100 Subject: [PATCH 085/175] restore affected files by clang format Signed-off-by: Wojciech Czerski --- .../Code/3rdParty/ImGuizmo/ImGuizmo.cpp | 6196 ++++++++--------- .../Code/3rdParty/ImGuizmo/ImGuizmo.h | 278 +- 2 files changed, 3105 insertions(+), 3369 deletions(-) diff --git a/Gems/ImGuizmo/Code/3rdParty/ImGuizmo/ImGuizmo.cpp b/Gems/ImGuizmo/Code/3rdParty/ImGuizmo/ImGuizmo.cpp index 49afc2b3..204e3d8b 100644 --- a/Gems/ImGuizmo/Code/3rdParty/ImGuizmo/ImGuizmo.cpp +++ b/Gems/ImGuizmo/Code/3rdParty/ImGuizmo/ImGuizmo.cpp @@ -27,9 +27,9 @@ #ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS #endif -#include "ImGuizmo.h" #include #include +#include "ImGuizmo.h" #if defined(_MSC_VER) || defined(__MINGW32__) #include @@ -45,1657 +45,1471 @@ namespace IMGUIZMO_NAMESPACE { - static const float ZPI = 3.14159265358979323846f; - static const float RAD2DEG = (180.f / ZPI); - static const float DEG2RAD = (ZPI / 180.f); - const float screenRotateSize = 0.06f; - // scale a bit so translate axis do not touch when in universal - const float rotationDisplayFactor = 1.2f; - - static OPERATION operator&(OPERATION lhs, OPERATION rhs) - { - return static_cast(static_cast(lhs) & static_cast(rhs)); - } - - static bool operator!=(OPERATION lhs, int rhs) - { - return static_cast(lhs) != rhs; - } - - static bool Intersects(OPERATION lhs, OPERATION rhs) - { - return (lhs & rhs) != 0; - } - - // True if lhs contains rhs - static bool Contains(OPERATION lhs, OPERATION rhs) - { - return (lhs & rhs) == rhs; - } - - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // utility and math - - void FPU_MatrixF_x_MatrixF(const float* a, const float* b, float* r) - { - r[0] = a[0] * b[0] + a[1] * b[4] + a[2] * b[8] + a[3] * b[12]; - r[1] = a[0] * b[1] + a[1] * b[5] + a[2] * b[9] + a[3] * b[13]; - r[2] = a[0] * b[2] + a[1] * b[6] + a[2] * b[10] + a[3] * b[14]; - r[3] = a[0] * b[3] + a[1] * b[7] + a[2] * b[11] + a[3] * b[15]; - - r[4] = a[4] * b[0] + a[5] * b[4] + a[6] * b[8] + a[7] * b[12]; - r[5] = a[4] * b[1] + a[5] * b[5] + a[6] * b[9] + a[7] * b[13]; - r[6] = a[4] * b[2] + a[5] * b[6] + a[6] * b[10] + a[7] * b[14]; - r[7] = a[4] * b[3] + a[5] * b[7] + a[6] * b[11] + a[7] * b[15]; - - r[8] = a[8] * b[0] + a[9] * b[4] + a[10] * b[8] + a[11] * b[12]; - r[9] = a[8] * b[1] + a[9] * b[5] + a[10] * b[9] + a[11] * b[13]; - r[10] = a[8] * b[2] + a[9] * b[6] + a[10] * b[10] + a[11] * b[14]; - r[11] = a[8] * b[3] + a[9] * b[7] + a[10] * b[11] + a[11] * b[15]; - - r[12] = a[12] * b[0] + a[13] * b[4] + a[14] * b[8] + a[15] * b[12]; - r[13] = a[12] * b[1] + a[13] * b[5] + a[14] * b[9] + a[15] * b[13]; - r[14] = a[12] * b[2] + a[13] * b[6] + a[14] * b[10] + a[15] * b[14]; - r[15] = a[12] * b[3] + a[13] * b[7] + a[14] * b[11] + a[15] * b[15]; - } - - void Frustum(float left, float right, float bottom, float top, float znear, float zfar, float* m16) - { - float temp, temp2, temp3, temp4; - temp = 2.0f * znear; - temp2 = right - left; - temp3 = top - bottom; - temp4 = zfar - znear; - m16[0] = temp / temp2; - m16[1] = 0.0; - m16[2] = 0.0; - m16[3] = 0.0; - m16[4] = 0.0; - m16[5] = temp / temp3; - m16[6] = 0.0; - m16[7] = 0.0; - m16[8] = (right + left) / temp2; - m16[9] = (top + bottom) / temp3; - m16[10] = (-zfar - znear) / temp4; - m16[11] = -1.0f; - m16[12] = 0.0; - m16[13] = 0.0; - m16[14] = (-temp * zfar) / temp4; - m16[15] = 0.0; - } - - void Perspective(float fovyInDegrees, float aspectRatio, float znear, float zfar, float* m16) - { - float ymax, xmax; - ymax = znear * tanf(fovyInDegrees * DEG2RAD); - xmax = ymax * aspectRatio; - Frustum(-xmax, xmax, -ymax, ymax, znear, zfar, m16); - } - - void Cross(const float* a, const float* b, float* r) - { - r[0] = a[1] * b[2] - a[2] * b[1]; - r[1] = a[2] * b[0] - a[0] * b[2]; - r[2] = a[0] * b[1] - a[1] * b[0]; - } - - float Dot(const float* a, const float* b) - { - return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; - } - - void Normalize(const float* a, float* r) - { - float il = 1.f / (sqrtf(Dot(a, a)) + FLT_EPSILON); - r[0] = a[0] * il; - r[1] = a[1] * il; - r[2] = a[2] * il; - } - - void LookAt(const float* eye, const float* at, const float* up, float* m16) - { - float X[3], Y[3], Z[3], tmp[3]; - - tmp[0] = eye[0] - at[0]; - tmp[1] = eye[1] - at[1]; - tmp[2] = eye[2] - at[2]; - Normalize(tmp, Z); - Normalize(up, Y); - Cross(Y, Z, tmp); - Normalize(tmp, X); - Cross(Z, X, tmp); - Normalize(tmp, Y); - - m16[0] = X[0]; - m16[1] = Y[0]; - m16[2] = Z[0]; - m16[3] = 0.0f; - m16[4] = X[1]; - m16[5] = Y[1]; - m16[6] = Z[1]; - m16[7] = 0.0f; - m16[8] = X[2]; - m16[9] = Y[2]; - m16[10] = Z[2]; - m16[11] = 0.0f; - m16[12] = -Dot(X, eye); - m16[13] = -Dot(Y, eye); - m16[14] = -Dot(Z, eye); - m16[15] = 1.0f; - } - - template - T Clamp(T x, T y, T z) - { - return ((x < y) ? y : ((x > z) ? z : x)); - } - template - T max(T x, T y) - { - return (x > y) ? x : y; - } - template - T min(T x, T y) - { - return (x < y) ? x : y; - } - template - bool IsWithin(T x, T y, T z) - { - return (x >= y) && (x <= z); - } - - struct matrix_t; - struct vec_t - { - public: - float x, y, z, w; - - void Lerp(const vec_t& v, float t) - { - x += (v.x - x) * t; - y += (v.y - y) * t; - z += (v.z - z) * t; - w += (v.w - w) * t; - } - - void Set(float v) - { - x = y = z = w = v; - } - void Set(float _x, float _y, float _z = 0.f, float _w = 0.f) - { - x = _x; - y = _y; - z = _z; - w = _w; - } - - vec_t& operator-=(const vec_t& v) - { - x -= v.x; - y -= v.y; - z -= v.z; - w -= v.w; - return *this; - } - vec_t& operator+=(const vec_t& v) - { - x += v.x; - y += v.y; - z += v.z; - w += v.w; - return *this; - } - vec_t& operator*=(const vec_t& v) - { - x *= v.x; - y *= v.y; - z *= v.z; - w *= v.w; - return *this; - } - vec_t& operator*=(float v) - { - x *= v; - y *= v; - z *= v; - w *= v; - return *this; - } - - vec_t operator*(float f) const; - vec_t operator-() const; - vec_t operator-(const vec_t& v) const; - vec_t operator+(const vec_t& v) const; - vec_t operator*(const vec_t& v) const; - - const vec_t& operator+() const - { - return (*this); - } - float Length() const - { - return sqrtf(x * x + y * y + z * z); - }; - float LengthSq() const - { - return (x * x + y * y + z * z); - }; - vec_t Normalize() - { - (*this) *= (1.f / (Length() > FLT_EPSILON ? Length() : FLT_EPSILON)); - return (*this); - } - vec_t Normalize(const vec_t& v) - { - this->Set(v.x, v.y, v.z, v.w); - this->Normalize(); - return (*this); - } - vec_t Abs() const; - - void Cross(const vec_t& v) - { - vec_t res; - res.x = y * v.z - z * v.y; - res.y = z * v.x - x * v.z; - res.z = x * v.y - y * v.x; - - x = res.x; - y = res.y; - z = res.z; - w = 0.f; - } - - void Cross(const vec_t& v1, const vec_t& v2) - { - x = v1.y * v2.z - v1.z * v2.y; - y = v1.z * v2.x - v1.x * v2.z; - z = v1.x * v2.y - v1.y * v2.x; - w = 0.f; - } - - float Dot(const vec_t& v) const - { - return (x * v.x) + (y * v.y) + (z * v.z) + (w * v.w); - } - - float Dot3(const vec_t& v) const - { - return (x * v.x) + (y * v.y) + (z * v.z); - } - - void Transform(const matrix_t& matrix); - void Transform(const vec_t& s, const matrix_t& matrix); - - void TransformVector(const matrix_t& matrix); - void TransformPoint(const matrix_t& matrix); - void TransformVector(const vec_t& v, const matrix_t& matrix) - { - (*this) = v; - this->TransformVector(matrix); - } - void TransformPoint(const vec_t& v, const matrix_t& matrix) - { - (*this) = v; - this->TransformPoint(matrix); - } - - float& operator[](size_t index) - { - return ((float*)&x)[index]; - } - const float& operator[](size_t index) const - { - return ((float*)&x)[index]; - } - bool operator!=(const vec_t& other) const - { - return memcmp(this, &other, sizeof(vec_t)) != 0; - } - }; - - vec_t makeVect(float _x, float _y, float _z = 0.f, float _w = 0.f) - { - vec_t res; - res.x = _x; - res.y = _y; - res.z = _z; - res.w = _w; - return res; - } - vec_t makeVect(ImVec2 v) - { - vec_t res; - res.x = v.x; - res.y = v.y; - res.z = 0.f; - res.w = 0.f; - return res; - } - vec_t vec_t::operator*(float f) const - { - return makeVect(x * f, y * f, z * f, w * f); - } - vec_t vec_t::operator-() const - { - return makeVect(-x, -y, -z, -w); - } - vec_t vec_t::operator-(const vec_t& v) const - { - return makeVect(x - v.x, y - v.y, z - v.z, w - v.w); - } - vec_t vec_t::operator+(const vec_t& v) const - { - return makeVect(x + v.x, y + v.y, z + v.z, w + v.w); - } - vec_t vec_t::operator*(const vec_t& v) const - { - return makeVect(x * v.x, y * v.y, z * v.z, w * v.w); - } - vec_t vec_t::Abs() const - { - return makeVect(fabsf(x), fabsf(y), fabsf(z)); - } - - vec_t Normalized(const vec_t& v) - { - vec_t res; - res = v; - res.Normalize(); - return res; - } - vec_t Cross(const vec_t& v1, const vec_t& v2) - { - vec_t res; - res.x = v1.y * v2.z - v1.z * v2.y; - res.y = v1.z * v2.x - v1.x * v2.z; - res.z = v1.x * v2.y - v1.y * v2.x; - res.w = 0.f; - return res; - } - - float Dot(const vec_t& v1, const vec_t& v2) - { - return (v1.x * v2.x) + (v1.y * v2.y) + (v1.z * v2.z); - } - - vec_t BuildPlan(const vec_t& p_point1, const vec_t& p_normal) - { - vec_t normal, res; - normal.Normalize(p_normal); - res.w = normal.Dot(p_point1); - res.x = normal.x; - res.y = normal.y; - res.z = normal.z; - return res; - } - - struct matrix_t - { - public: - union { - float m[4][4]; - float m16[16]; - struct - { - vec_t right, up, dir, position; - } v; - vec_t component[4]; - }; - - operator float*() - { - return m16; - } - operator const float*() const - { - return m16; - } - void Translation(float _x, float _y, float _z) - { - this->Translation(makeVect(_x, _y, _z)); - } - - void Translation(const vec_t& vt) - { - v.right.Set(1.f, 0.f, 0.f, 0.f); - v.up.Set(0.f, 1.f, 0.f, 0.f); - v.dir.Set(0.f, 0.f, 1.f, 0.f); - v.position.Set(vt.x, vt.y, vt.z, 1.f); - } - - void Scale(float _x, float _y, float _z) - { - v.right.Set(_x, 0.f, 0.f, 0.f); - v.up.Set(0.f, _y, 0.f, 0.f); - v.dir.Set(0.f, 0.f, _z, 0.f); - v.position.Set(0.f, 0.f, 0.f, 1.f); - } - void Scale(const vec_t& s) - { - Scale(s.x, s.y, s.z); - } - - matrix_t& operator*=(const matrix_t& mat) - { - matrix_t tmpMat; - tmpMat = *this; - tmpMat.Multiply(mat); - *this = tmpMat; - return *this; - } - matrix_t operator*(const matrix_t& mat) const - { - matrix_t matT; - matT.Multiply(*this, mat); - return matT; - } - - void Multiply(const matrix_t& matrix) - { - matrix_t tmp; - tmp = *this; - - FPU_MatrixF_x_MatrixF((float*)&tmp, (float*)&matrix, (float*)this); - } - - void Multiply(const matrix_t& m1, const matrix_t& m2) - { - FPU_MatrixF_x_MatrixF((float*)&m1, (float*)&m2, (float*)this); - } - - float GetDeterminant() const - { - return m[0][0] * m[1][1] * m[2][2] + m[0][1] * m[1][2] * m[2][0] + m[0][2] * m[1][0] * m[2][1] - m[0][2] * m[1][1] * m[2][0] - - m[0][1] * m[1][0] * m[2][2] - m[0][0] * m[1][2] * m[2][1]; - } - - float Inverse(const matrix_t& srcMatrix, bool affine = false); - void SetToIdentity() - { - v.right.Set(1.f, 0.f, 0.f, 0.f); - v.up.Set(0.f, 1.f, 0.f, 0.f); - v.dir.Set(0.f, 0.f, 1.f, 0.f); - v.position.Set(0.f, 0.f, 0.f, 1.f); - } - void Transpose() - { - matrix_t tmpm; - for (int l = 0; l < 4; l++) - { - for (int c = 0; c < 4; c++) - { - tmpm.m[l][c] = m[c][l]; - } - } - (*this) = tmpm; - } - - void RotationAxis(const vec_t& axis, float angle); - - void OrthoNormalize() - { - v.right.Normalize(); - v.up.Normalize(); - v.dir.Normalize(); - } - }; - - void vec_t::Transform(const matrix_t& matrix) - { - vec_t out; - - out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0] + w * matrix.m[3][0]; - out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1] + w * matrix.m[3][1]; - out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2] + w * matrix.m[3][2]; - out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3] + w * matrix.m[3][3]; - - x = out.x; - y = out.y; - z = out.z; - w = out.w; - } - - void vec_t::Transform(const vec_t& s, const matrix_t& matrix) - { - *this = s; - Transform(matrix); - } - - void vec_t::TransformPoint(const matrix_t& matrix) - { - vec_t out; - - out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0] + matrix.m[3][0]; - out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1] + matrix.m[3][1]; - out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2] + matrix.m[3][2]; - out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3] + matrix.m[3][3]; - - x = out.x; - y = out.y; - z = out.z; - w = out.w; - } - - void vec_t::TransformVector(const matrix_t& matrix) - { - vec_t out; - - out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0]; - out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1]; - out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2]; - out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3]; - - x = out.x; - y = out.y; - z = out.z; - w = out.w; - } - - float matrix_t::Inverse(const matrix_t& srcMatrix, bool affine) - { - float det = 0; - - if (affine) - { - det = GetDeterminant(); - float s = 1 / det; - m[0][0] = (srcMatrix.m[1][1] * srcMatrix.m[2][2] - srcMatrix.m[1][2] * srcMatrix.m[2][1]) * s; - m[0][1] = (srcMatrix.m[2][1] * srcMatrix.m[0][2] - srcMatrix.m[2][2] * srcMatrix.m[0][1]) * s; - m[0][2] = (srcMatrix.m[0][1] * srcMatrix.m[1][2] - srcMatrix.m[0][2] * srcMatrix.m[1][1]) * s; - m[1][0] = (srcMatrix.m[1][2] * srcMatrix.m[2][0] - srcMatrix.m[1][0] * srcMatrix.m[2][2]) * s; - m[1][1] = (srcMatrix.m[2][2] * srcMatrix.m[0][0] - srcMatrix.m[2][0] * srcMatrix.m[0][2]) * s; - m[1][2] = (srcMatrix.m[0][2] * srcMatrix.m[1][0] - srcMatrix.m[0][0] * srcMatrix.m[1][2]) * s; - m[2][0] = (srcMatrix.m[1][0] * srcMatrix.m[2][1] - srcMatrix.m[1][1] * srcMatrix.m[2][0]) * s; - m[2][1] = (srcMatrix.m[2][0] * srcMatrix.m[0][1] - srcMatrix.m[2][1] * srcMatrix.m[0][0]) * s; - m[2][2] = (srcMatrix.m[0][0] * srcMatrix.m[1][1] - srcMatrix.m[0][1] * srcMatrix.m[1][0]) * s; - m[3][0] = -(m[0][0] * srcMatrix.m[3][0] + m[1][0] * srcMatrix.m[3][1] + m[2][0] * srcMatrix.m[3][2]); - m[3][1] = -(m[0][1] * srcMatrix.m[3][0] + m[1][1] * srcMatrix.m[3][1] + m[2][1] * srcMatrix.m[3][2]); - m[3][2] = -(m[0][2] * srcMatrix.m[3][0] + m[1][2] * srcMatrix.m[3][1] + m[2][2] * srcMatrix.m[3][2]); - } - else - { - // transpose matrix - float src[16]; - for (int i = 0; i < 4; ++i) - { - src[i] = srcMatrix.m16[i * 4]; - src[i + 4] = srcMatrix.m16[i * 4 + 1]; - src[i + 8] = srcMatrix.m16[i * 4 + 2]; - src[i + 12] = srcMatrix.m16[i * 4 + 3]; - } - - // calculate pairs for first 8 elements (cofactors) - float tmp[12]; // temp array for pairs - tmp[0] = src[10] * src[15]; - tmp[1] = src[11] * src[14]; - tmp[2] = src[9] * src[15]; - tmp[3] = src[11] * src[13]; - tmp[4] = src[9] * src[14]; - tmp[5] = src[10] * src[13]; - tmp[6] = src[8] * src[15]; - tmp[7] = src[11] * src[12]; - tmp[8] = src[8] * src[14]; - tmp[9] = src[10] * src[12]; - tmp[10] = src[8] * src[13]; - tmp[11] = src[9] * src[12]; - - // calculate first 8 elements (cofactors) - m16[0] = (tmp[0] * src[5] + tmp[3] * src[6] + tmp[4] * src[7]) - (tmp[1] * src[5] + tmp[2] * src[6] + tmp[5] * src[7]); - m16[1] = (tmp[1] * src[4] + tmp[6] * src[6] + tmp[9] * src[7]) - (tmp[0] * src[4] + tmp[7] * src[6] + tmp[8] * src[7]); - m16[2] = (tmp[2] * src[4] + tmp[7] * src[5] + tmp[10] * src[7]) - (tmp[3] * src[4] + tmp[6] * src[5] + tmp[11] * src[7]); - m16[3] = (tmp[5] * src[4] + tmp[8] * src[5] + tmp[11] * src[6]) - (tmp[4] * src[4] + tmp[9] * src[5] + tmp[10] * src[6]); - m16[4] = (tmp[1] * src[1] + tmp[2] * src[2] + tmp[5] * src[3]) - (tmp[0] * src[1] + tmp[3] * src[2] + tmp[4] * src[3]); - m16[5] = (tmp[0] * src[0] + tmp[7] * src[2] + tmp[8] * src[3]) - (tmp[1] * src[0] + tmp[6] * src[2] + tmp[9] * src[3]); - m16[6] = (tmp[3] * src[0] + tmp[6] * src[1] + tmp[11] * src[3]) - (tmp[2] * src[0] + tmp[7] * src[1] + tmp[10] * src[3]); - m16[7] = (tmp[4] * src[0] + tmp[9] * src[1] + tmp[10] * src[2]) - (tmp[5] * src[0] + tmp[8] * src[1] + tmp[11] * src[2]); - - // calculate pairs for second 8 elements (cofactors) - tmp[0] = src[2] * src[7]; - tmp[1] = src[3] * src[6]; - tmp[2] = src[1] * src[7]; - tmp[3] = src[3] * src[5]; - tmp[4] = src[1] * src[6]; - tmp[5] = src[2] * src[5]; - tmp[6] = src[0] * src[7]; - tmp[7] = src[3] * src[4]; - tmp[8] = src[0] * src[6]; - tmp[9] = src[2] * src[4]; - tmp[10] = src[0] * src[5]; - tmp[11] = src[1] * src[4]; - - // calculate second 8 elements (cofactors) - m16[8] = (tmp[0] * src[13] + tmp[3] * src[14] + tmp[4] * src[15]) - (tmp[1] * src[13] + tmp[2] * src[14] + tmp[5] * src[15]); - m16[9] = (tmp[1] * src[12] + tmp[6] * src[14] + tmp[9] * src[15]) - (tmp[0] * src[12] + tmp[7] * src[14] + tmp[8] * src[15]); - m16[10] = (tmp[2] * src[12] + tmp[7] * src[13] + tmp[10] * src[15]) - (tmp[3] * src[12] + tmp[6] * src[13] + tmp[11] * src[15]); - m16[11] = (tmp[5] * src[12] + tmp[8] * src[13] + tmp[11] * src[14]) - (tmp[4] * src[12] + tmp[9] * src[13] + tmp[10] * src[14]); - m16[12] = (tmp[2] * src[10] + tmp[5] * src[11] + tmp[1] * src[9]) - (tmp[4] * src[11] + tmp[0] * src[9] + tmp[3] * src[10]); - m16[13] = (tmp[8] * src[11] + tmp[0] * src[8] + tmp[7] * src[10]) - (tmp[6] * src[10] + tmp[9] * src[11] + tmp[1] * src[8]); - m16[14] = (tmp[6] * src[9] + tmp[11] * src[11] + tmp[3] * src[8]) - (tmp[10] * src[11] + tmp[2] * src[8] + tmp[7] * src[9]); - m16[15] = (tmp[10] * src[10] + tmp[4] * src[8] + tmp[9] * src[9]) - (tmp[8] * src[9] + tmp[11] * src[10] + tmp[5] * src[8]); - - // calculate determinant - det = src[0] * m16[0] + src[1] * m16[1] + src[2] * m16[2] + src[3] * m16[3]; - - // calculate matrix inverse - float invdet = 1 / det; - for (int j = 0; j < 16; ++j) - { - m16[j] *= invdet; - } - } - - return det; - } - - void matrix_t::RotationAxis(const vec_t& axis, float angle) - { - float length2 = axis.LengthSq(); - if (length2 < FLT_EPSILON) - { - SetToIdentity(); - return; - } - - vec_t n = axis * (1.f / sqrtf(length2)); - float s = sinf(angle); - float c = cosf(angle); - float k = 1.f - c; - - float xx = n.x * n.x * k + c; - float yy = n.y * n.y * k + c; - float zz = n.z * n.z * k + c; - float xy = n.x * n.y * k; - float yz = n.y * n.z * k; - float zx = n.z * n.x * k; - float xs = n.x * s; - float ys = n.y * s; - float zs = n.z * s; - - m[0][0] = xx; - m[0][1] = xy + zs; - m[0][2] = zx - ys; - m[0][3] = 0.f; - m[1][0] = xy - zs; - m[1][1] = yy; - m[1][2] = yz + xs; - m[1][3] = 0.f; - m[2][0] = zx + ys; - m[2][1] = yz - xs; - m[2][2] = zz; - m[2][3] = 0.f; - m[3][0] = 0.f; - m[3][1] = 0.f; - m[3][2] = 0.f; - m[3][3] = 1.f; - } - - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // - - enum MOVETYPE - { - MT_NONE, - MT_MOVE_X, - MT_MOVE_Y, - MT_MOVE_Z, - MT_MOVE_YZ, - MT_MOVE_ZX, - MT_MOVE_XY, - MT_MOVE_SCREEN, - MT_ROTATE_X, - MT_ROTATE_Y, - MT_ROTATE_Z, - MT_ROTATE_SCREEN, - MT_SCALE_X, - MT_SCALE_Y, - MT_SCALE_Z, - MT_SCALE_XYZ - }; - - static bool IsTranslateType(int type) - { - return type >= MT_MOVE_X && type <= MT_MOVE_SCREEN; - } - - static bool IsRotateType(int type) - { - return type >= MT_ROTATE_X && type <= MT_ROTATE_SCREEN; - } - - static bool IsScaleType(int type) - { - return type >= MT_SCALE_X && type <= MT_SCALE_XYZ; - } - - // Matches MT_MOVE_AB order - static const OPERATION TRANSLATE_PLANS[3] = { TRANSLATE_Y | TRANSLATE_Z, TRANSLATE_X | TRANSLATE_Z, TRANSLATE_X | TRANSLATE_Y }; - - Style::Style() - { - // default values - TranslationLineThickness = 3.0f; - TranslationLineArrowSize = 6.0f; - RotationLineThickness = 2.0f; - RotationOuterLineThickness = 3.0f; - ScaleLineThickness = 3.0f; - ScaleLineCircleSize = 6.0f; - HatchedAxisLineThickness = 6.0f; - CenterCircleSize = 6.0f; - - // initialize default colors - Colors[DIRECTION_X] = ImVec4(0.666f, 0.000f, 0.000f, 1.000f); - Colors[DIRECTION_Y] = ImVec4(0.000f, 0.666f, 0.000f, 1.000f); - Colors[DIRECTION_Z] = ImVec4(0.000f, 0.000f, 0.666f, 1.000f); - Colors[PLANE_X] = ImVec4(0.666f, 0.000f, 0.000f, 0.380f); - Colors[PLANE_Y] = ImVec4(0.000f, 0.666f, 0.000f, 0.380f); - Colors[PLANE_Z] = ImVec4(0.000f, 0.000f, 0.666f, 0.380f); - Colors[SELECTION] = ImVec4(1.000f, 0.500f, 0.062f, 0.541f); - Colors[INACTIVE] = ImVec4(0.600f, 0.600f, 0.600f, 0.600f); - Colors[TRANSLATION_LINE] = ImVec4(0.666f, 0.666f, 0.666f, 0.666f); - Colors[SCALE_LINE] = ImVec4(0.250f, 0.250f, 0.250f, 1.000f); - Colors[ROTATION_USING_BORDER] = ImVec4(1.000f, 0.500f, 0.062f, 1.000f); - Colors[ROTATION_USING_FILL] = ImVec4(1.000f, 0.500f, 0.062f, 0.500f); - Colors[HATCHED_AXIS_LINES] = ImVec4(0.000f, 0.000f, 0.000f, 0.500f); - Colors[TEXT] = ImVec4(1.000f, 1.000f, 1.000f, 1.000f); - Colors[TEXT_SHADOW] = ImVec4(0.000f, 0.000f, 0.000f, 1.000f); - } - - struct Context - { - Context() - : mbUsing(false) - , mbUsingViewManipulate(false) - , mbEnable(true) - , mIsViewManipulatorHovered(false) - , mbUsingBounds(false) - { - } - - ImDrawList* mDrawList; - Style mStyle; - - MODE mMode; - matrix_t mViewMat; - matrix_t mProjectionMat; - matrix_t mModel; - matrix_t mModelLocal; // orthonormalized model - matrix_t mModelInverse; - matrix_t mModelSource; - matrix_t mModelSourceInverse; - matrix_t mMVP; - matrix_t - mMVPLocal; // MVP with full model matrix whereas mMVP's model matrix might only be translation in case of World space edition - matrix_t mViewProjection; - - vec_t mModelScaleOrigin; - vec_t mCameraEye; - vec_t mCameraRight; - vec_t mCameraDir; - vec_t mCameraUp; - vec_t mRayOrigin; - vec_t mRayVector; - - float mRadiusSquareCenter; - ImVec2 mScreenSquareCenter; - ImVec2 mScreenSquareMin; - ImVec2 mScreenSquareMax; - - float mScreenFactor; - vec_t mRelativeOrigin; - - bool mbUsing; - bool mbUsingViewManipulate; - bool mbEnable; - bool mbMouseOver; - bool mReversed; // reversed projection matrix - bool mIsViewManipulatorHovered; - - // translation - vec_t mTranslationPlan; - vec_t mTranslationPlanOrigin; - vec_t mMatrixOrigin; - vec_t mTranslationLastDelta; - - // rotation - vec_t mRotationVectorSource; - float mRotationAngle; - float mRotationAngleOrigin; - // vec_t mWorldToLocalAxis; - - // scale - vec_t mScale; - vec_t mScaleValueOrigin; - vec_t mScaleLast; - float mSaveMousePosx; - - // save axis factor when using gizmo - bool mBelowAxisLimit[3]; - int mAxisMask = 0; - bool mBelowPlaneLimit[3]; - float mAxisFactor[3]; - - float mAxisLimit = 0.0025f; - float mPlaneLimit = 0.02f; - - // bounds stretching - vec_t mBoundsPivot; - vec_t mBoundsAnchor; - vec_t mBoundsPlan; - vec_t mBoundsLocalPivot; - int mBoundsBestAxis; - int mBoundsAxis[2]; - bool mbUsingBounds; - matrix_t mBoundsMatrix; - - // - int mCurrentOperation; - - float mX = 0.f; - float mY = 0.f; - float mWidth = 0.f; - float mHeight = 0.f; - float mXMax = 0.f; - float mYMax = 0.f; - float mDisplayRatio = 1.f; - - bool mIsOrthographic = false; - // check to not have multiple gizmo highlighted at the same time - bool mbOverGizmoHotspot = false; - - ImGuiWindow* mAlternativeWindow = nullptr; - ImVector mIDStack; - ImGuiID mEditingID = -1; - OPERATION mOperation = OPERATION(-1); - - bool mAllowAxisFlip = true; - float mGizmoSizeClipSpace = 0.1f; - - inline ImGuiID GetCurrentID() - { - if (mIDStack.empty()) - { - mIDStack.push_back(-1); - } - return mIDStack.back(); - } - }; - - static Context gContext; - - static const vec_t directionUnary[3] = { makeVect(1.f, 0.f, 0.f), makeVect(0.f, 1.f, 0.f), makeVect(0.f, 0.f, 1.f) }; - static const char* translationInfoMask[] = { "X : %5.3f", - "Y : %5.3f", - "Z : %5.3f", - "Y : %5.3f Z : %5.3f", - "X : %5.3f Z : %5.3f", - "X : %5.3f Y : %5.3f", - "X : %5.3f Y : %5.3f Z : %5.3f" }; - static const char* scaleInfoMask[] = { "X : %5.2f", "Y : %5.2f", "Z : %5.2f", "XYZ : %5.2f" }; - static const char* rotationInfoMask[] = { - "X : %5.2f deg %5.2f rad", "Y : %5.2f deg %5.2f rad", "Z : %5.2f deg %5.2f rad", "Screen : %5.2f deg %5.2f rad" - }; - static const int translationInfoIndex[] = { 0, 0, 0, 1, 0, 0, 2, 0, 0, 1, 2, 0, 0, 2, 0, 0, 1, 0, 0, 1, 2 }; - static const float quadMin = 0.5f; - static const float quadMax = 0.8f; - static const float quadUV[8] = { quadMin, quadMin, quadMin, quadMax, quadMax, quadMax, quadMax, quadMin }; - static const int halfCircleSegmentCount = 64; - static const float snapTension = 0.5f; - - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // - static int GetMoveType(OPERATION op, vec_t* gizmoHitProportion); - static int GetRotateType(OPERATION op); - static int GetScaleType(OPERATION op); - - Style& GetStyle() - { - return gContext.mStyle; - } - - static ImU32 GetColorU32(int idx) - { - IM_ASSERT(idx < COLOR::COUNT); - return ImGui::ColorConvertFloat4ToU32(gContext.mStyle.Colors[idx]); - } - - static ImVec2 worldToPos( - const vec_t& worldPos, - const matrix_t& mat, - ImVec2 position = ImVec2(gContext.mX, gContext.mY), - ImVec2 size = ImVec2(gContext.mWidth, gContext.mHeight)) - { - vec_t trans; - trans.TransformPoint(worldPos, mat); - trans *= 0.5f / trans.w; - trans += makeVect(0.5f, 0.5f); - trans.y = 1.f - trans.y; - trans.x *= size.x; - trans.y *= size.y; - trans.x += position.x; - trans.y += position.y; - return ImVec2(trans.x, trans.y); - } - - static void ComputeCameraRay( - vec_t& rayOrigin, - vec_t& rayDir, - ImVec2 position = ImVec2(gContext.mX, gContext.mY), - ImVec2 size = ImVec2(gContext.mWidth, gContext.mHeight)) - { - ImGuiIO& io = ImGui::GetIO(); - - matrix_t mViewProjInverse; - mViewProjInverse.Inverse(gContext.mViewMat * gContext.mProjectionMat); - - const float mox = ((io.MousePos.x - position.x) / size.x) * 2.f - 1.f; - const float moy = (1.f - ((io.MousePos.y - position.y) / size.y)) * 2.f - 1.f; - - const float zNear = gContext.mReversed ? (1.f - FLT_EPSILON) : 0.f; - const float zFar = gContext.mReversed ? 0.f : (1.f - FLT_EPSILON); - - rayOrigin.Transform(makeVect(mox, moy, zNear, 1.f), mViewProjInverse); - rayOrigin *= 1.f / rayOrigin.w; - vec_t rayEnd; - rayEnd.Transform(makeVect(mox, moy, zFar, 1.f), mViewProjInverse); - rayEnd *= 1.f / rayEnd.w; - rayDir = Normalized(rayEnd - rayOrigin); - } - - static float GetSegmentLengthClipSpace(const vec_t& start, const vec_t& end, const bool localCoordinates = false) - { - vec_t startOfSegment = start; - const matrix_t& mvp = localCoordinates ? gContext.mMVPLocal : gContext.mMVP; - startOfSegment.TransformPoint(mvp); - if (fabsf(startOfSegment.w) > FLT_EPSILON) // check for axis aligned with camera direction - { - startOfSegment *= 1.f / startOfSegment.w; - } - - vec_t endOfSegment = end; - endOfSegment.TransformPoint(mvp); - if (fabsf(endOfSegment.w) > FLT_EPSILON) // check for axis aligned with camera direction - { - endOfSegment *= 1.f / endOfSegment.w; - } - - vec_t clipSpaceAxis = endOfSegment - startOfSegment; - if (gContext.mDisplayRatio < 1.0) - clipSpaceAxis.x *= gContext.mDisplayRatio; - else - clipSpaceAxis.y /= gContext.mDisplayRatio; - float segmentLengthInClipSpace = sqrtf(clipSpaceAxis.x * clipSpaceAxis.x + clipSpaceAxis.y * clipSpaceAxis.y); - return segmentLengthInClipSpace; - } - - static float GetParallelogram(const vec_t& ptO, const vec_t& ptA, const vec_t& ptB) - { - vec_t pts[] = { ptO, ptA, ptB }; - for (unsigned int i = 0; i < 3; i++) - { - pts[i].TransformPoint(gContext.mMVP); - if (fabsf(pts[i].w) > FLT_EPSILON) // check for axis aligned with camera direction - { - pts[i] *= 1.f / pts[i].w; - } - } - vec_t segA = pts[1] - pts[0]; - vec_t segB = pts[2] - pts[0]; - segA.y /= gContext.mDisplayRatio; - segB.y /= gContext.mDisplayRatio; - vec_t segAOrtho = makeVect(-segA.y, segA.x); - segAOrtho.Normalize(); - float dt = segAOrtho.Dot3(segB); - float surface = sqrtf(segA.x * segA.x + segA.y * segA.y) * fabsf(dt); - return surface; - } - - inline vec_t PointOnSegment(const vec_t& point, const vec_t& vertPos1, const vec_t& vertPos2) - { - vec_t c = point - vertPos1; - vec_t V; - - V.Normalize(vertPos2 - vertPos1); - float d = (vertPos2 - vertPos1).Length(); - float t = V.Dot3(c); - - if (t < 0.f) - { - return vertPos1; - } - - if (t > d) - { - return vertPos2; - } - - return vertPos1 + V * t; - } - - static float IntersectRayPlane(const vec_t& rOrigin, const vec_t& rVector, const vec_t& plan) - { - const float numer = plan.Dot3(rOrigin) - plan.w; - const float denom = plan.Dot3(rVector); - - if (fabsf(denom) < FLT_EPSILON) // normal is orthogonal to vector, cant intersect - { - return -1.0f; - } - - return -(numer / denom); - } - - static float DistanceToPlane(const vec_t& point, const vec_t& plan) - { - return plan.Dot3(point) + plan.w; - } - - static bool IsInContextRect(ImVec2 p) - { - return IsWithin(p.x, gContext.mX, gContext.mXMax) && IsWithin(p.y, gContext.mY, gContext.mYMax); - } - - static bool IsHoveringWindow() - { - ImGuiContext& g = *ImGui::GetCurrentContext(); - ImGuiWindow* window = ImGui::FindWindowByName(gContext.mDrawList->_OwnerName); - if (g.HoveredWindow == window) // Mouse hovering drawlist window - return true; - if (gContext.mAlternativeWindow != nullptr && g.HoveredWindow == gContext.mAlternativeWindow) - return true; - if (g.HoveredWindow != NULL) // Any other window is hovered - return false; - if (ImGui::IsMouseHoveringRect( - window->InnerRect.Min, - window->InnerRect.Max, - false)) // Hovering drawlist window rect, while no other window is hovered (for _NoInputs windows) - return true; - return false; - } - - void SetRect(float x, float y, float width, float height) - { - gContext.mX = x; - gContext.mY = y; - gContext.mWidth = width; - gContext.mHeight = height; - gContext.mXMax = gContext.mX + gContext.mWidth; - gContext.mYMax = gContext.mY + gContext.mXMax; - gContext.mDisplayRatio = width / height; - } - - void SetOrthographic(bool isOrthographic) - { - gContext.mIsOrthographic = isOrthographic; - } - - void SetDrawlist(ImDrawList* drawlist) - { - gContext.mDrawList = drawlist ? drawlist : ImGui::GetWindowDrawList(); - } - - void SetImGuiContext(ImGuiContext* ctx) - { - ImGui::SetCurrentContext(ctx); - } - - void BeginFrame() - { - const ImU32 flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | - ImGuiWindowFlags_NoBringToFrontOnFocus; + static const float ZPI = 3.14159265358979323846f; + static const float RAD2DEG = (180.f / ZPI); + static const float DEG2RAD = (ZPI / 180.f); + const float screenRotateSize = 0.06f; + // scale a bit so translate axis do not touch when in universal + const float rotationDisplayFactor = 1.2f; + + static OPERATION operator&(OPERATION lhs, OPERATION rhs) + { + return static_cast(static_cast(lhs) & static_cast(rhs)); + } + + static bool operator!=(OPERATION lhs, int rhs) + { + return static_cast(lhs) != rhs; + } + + static bool Intersects(OPERATION lhs, OPERATION rhs) + { + return (lhs & rhs) != 0; + } + + // True if lhs contains rhs + static bool Contains(OPERATION lhs, OPERATION rhs) + { + return (lhs & rhs) == rhs; + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // utility and math + + void FPU_MatrixF_x_MatrixF(const float* a, const float* b, float* r) + { + r[0] = a[0] * b[0] + a[1] * b[4] + a[2] * b[8] + a[3] * b[12]; + r[1] = a[0] * b[1] + a[1] * b[5] + a[2] * b[9] + a[3] * b[13]; + r[2] = a[0] * b[2] + a[1] * b[6] + a[2] * b[10] + a[3] * b[14]; + r[3] = a[0] * b[3] + a[1] * b[7] + a[2] * b[11] + a[3] * b[15]; + + r[4] = a[4] * b[0] + a[5] * b[4] + a[6] * b[8] + a[7] * b[12]; + r[5] = a[4] * b[1] + a[5] * b[5] + a[6] * b[9] + a[7] * b[13]; + r[6] = a[4] * b[2] + a[5] * b[6] + a[6] * b[10] + a[7] * b[14]; + r[7] = a[4] * b[3] + a[5] * b[7] + a[6] * b[11] + a[7] * b[15]; + + r[8] = a[8] * b[0] + a[9] * b[4] + a[10] * b[8] + a[11] * b[12]; + r[9] = a[8] * b[1] + a[9] * b[5] + a[10] * b[9] + a[11] * b[13]; + r[10] = a[8] * b[2] + a[9] * b[6] + a[10] * b[10] + a[11] * b[14]; + r[11] = a[8] * b[3] + a[9] * b[7] + a[10] * b[11] + a[11] * b[15]; + + r[12] = a[12] * b[0] + a[13] * b[4] + a[14] * b[8] + a[15] * b[12]; + r[13] = a[12] * b[1] + a[13] * b[5] + a[14] * b[9] + a[15] * b[13]; + r[14] = a[12] * b[2] + a[13] * b[6] + a[14] * b[10] + a[15] * b[14]; + r[15] = a[12] * b[3] + a[13] * b[7] + a[14] * b[11] + a[15] * b[15]; + } + + void Frustum(float left, float right, float bottom, float top, float znear, float zfar, float* m16) + { + float temp, temp2, temp3, temp4; + temp = 2.0f * znear; + temp2 = right - left; + temp3 = top - bottom; + temp4 = zfar - znear; + m16[0] = temp / temp2; + m16[1] = 0.0; + m16[2] = 0.0; + m16[3] = 0.0; + m16[4] = 0.0; + m16[5] = temp / temp3; + m16[6] = 0.0; + m16[7] = 0.0; + m16[8] = (right + left) / temp2; + m16[9] = (top + bottom) / temp3; + m16[10] = (-zfar - znear) / temp4; + m16[11] = -1.0f; + m16[12] = 0.0; + m16[13] = 0.0; + m16[14] = (-temp * zfar) / temp4; + m16[15] = 0.0; + } + + void Perspective(float fovyInDegrees, float aspectRatio, float znear, float zfar, float* m16) + { + float ymax, xmax; + ymax = znear * tanf(fovyInDegrees * DEG2RAD); + xmax = ymax * aspectRatio; + Frustum(-xmax, xmax, -ymax, ymax, znear, zfar, m16); + } + + void Cross(const float* a, const float* b, float* r) + { + r[0] = a[1] * b[2] - a[2] * b[1]; + r[1] = a[2] * b[0] - a[0] * b[2]; + r[2] = a[0] * b[1] - a[1] * b[0]; + } + + float Dot(const float* a, const float* b) + { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + } + + void Normalize(const float* a, float* r) + { + float il = 1.f / (sqrtf(Dot(a, a)) + FLT_EPSILON); + r[0] = a[0] * il; + r[1] = a[1] * il; + r[2] = a[2] * il; + } + + void LookAt(const float* eye, const float* at, const float* up, float* m16) + { + float X[3], Y[3], Z[3], tmp[3]; + + tmp[0] = eye[0] - at[0]; + tmp[1] = eye[1] - at[1]; + tmp[2] = eye[2] - at[2]; + Normalize(tmp, Z); + Normalize(up, Y); + Cross(Y, Z, tmp); + Normalize(tmp, X); + Cross(Z, X, tmp); + Normalize(tmp, Y); + + m16[0] = X[0]; + m16[1] = Y[0]; + m16[2] = Z[0]; + m16[3] = 0.0f; + m16[4] = X[1]; + m16[5] = Y[1]; + m16[6] = Z[1]; + m16[7] = 0.0f; + m16[8] = X[2]; + m16[9] = Y[2]; + m16[10] = Z[2]; + m16[11] = 0.0f; + m16[12] = -Dot(X, eye); + m16[13] = -Dot(Y, eye); + m16[14] = -Dot(Z, eye); + m16[15] = 1.0f; + } + + template T Clamp(T x, T y, T z) { return ((x < y) ? y : ((x > z) ? z : x)); } + template T max(T x, T y) { return (x > y) ? x : y; } + template T min(T x, T y) { return (x < y) ? x : y; } + template bool IsWithin(T x, T y, T z) { return (x >= y) && (x <= z); } + + struct matrix_t; + struct vec_t + { + public: + float x, y, z, w; + + void Lerp(const vec_t& v, float t) + { + x += (v.x - x) * t; + y += (v.y - y) * t; + z += (v.z - z) * t; + w += (v.w - w) * t; + } + + void Set(float v) { x = y = z = w = v; } + void Set(float _x, float _y, float _z = 0.f, float _w = 0.f) { x = _x; y = _y; z = _z; w = _w; } + + vec_t& operator -= (const vec_t& v) { x -= v.x; y -= v.y; z -= v.z; w -= v.w; return *this; } + vec_t& operator += (const vec_t& v) { x += v.x; y += v.y; z += v.z; w += v.w; return *this; } + vec_t& operator *= (const vec_t& v) { x *= v.x; y *= v.y; z *= v.z; w *= v.w; return *this; } + vec_t& operator *= (float v) { x *= v; y *= v; z *= v; w *= v; return *this; } + + vec_t operator * (float f) const; + vec_t operator - () const; + vec_t operator - (const vec_t& v) const; + vec_t operator + (const vec_t& v) const; + vec_t operator * (const vec_t& v) const; + + const vec_t& operator + () const { return (*this); } + float Length() const { return sqrtf(x * x + y * y + z * z); }; + float LengthSq() const { return (x * x + y * y + z * z); }; + vec_t Normalize() { (*this) *= (1.f / ( Length() > FLT_EPSILON ? Length() : FLT_EPSILON ) ); return (*this); } + vec_t Normalize(const vec_t& v) { this->Set(v.x, v.y, v.z, v.w); this->Normalize(); return (*this); } + vec_t Abs() const; + + void Cross(const vec_t& v) + { + vec_t res; + res.x = y * v.z - z * v.y; + res.y = z * v.x - x * v.z; + res.z = x * v.y - y * v.x; + + x = res.x; + y = res.y; + z = res.z; + w = 0.f; + } + + void Cross(const vec_t& v1, const vec_t& v2) + { + x = v1.y * v2.z - v1.z * v2.y; + y = v1.z * v2.x - v1.x * v2.z; + z = v1.x * v2.y - v1.y * v2.x; + w = 0.f; + } + + float Dot(const vec_t& v) const + { + return (x * v.x) + (y * v.y) + (z * v.z) + (w * v.w); + } + + float Dot3(const vec_t& v) const + { + return (x * v.x) + (y * v.y) + (z * v.z); + } + + void Transform(const matrix_t& matrix); + void Transform(const vec_t& s, const matrix_t& matrix); + + void TransformVector(const matrix_t& matrix); + void TransformPoint(const matrix_t& matrix); + void TransformVector(const vec_t& v, const matrix_t& matrix) { (*this) = v; this->TransformVector(matrix); } + void TransformPoint(const vec_t& v, const matrix_t& matrix) { (*this) = v; this->TransformPoint(matrix); } + + float& operator [] (size_t index) { return ((float*)&x)[index]; } + const float& operator [] (size_t index) const { return ((float*)&x)[index]; } + bool operator!=(const vec_t& other) const { return memcmp(this, &other, sizeof(vec_t)) != 0; } + }; + + vec_t makeVect(float _x, float _y, float _z = 0.f, float _w = 0.f) { vec_t res; res.x = _x; res.y = _y; res.z = _z; res.w = _w; return res; } + vec_t makeVect(ImVec2 v) { vec_t res; res.x = v.x; res.y = v.y; res.z = 0.f; res.w = 0.f; return res; } + vec_t vec_t::operator * (float f) const { return makeVect(x * f, y * f, z * f, w * f); } + vec_t vec_t::operator - () const { return makeVect(-x, -y, -z, -w); } + vec_t vec_t::operator - (const vec_t& v) const { return makeVect(x - v.x, y - v.y, z - v.z, w - v.w); } + vec_t vec_t::operator + (const vec_t& v) const { return makeVect(x + v.x, y + v.y, z + v.z, w + v.w); } + vec_t vec_t::operator * (const vec_t& v) const { return makeVect(x * v.x, y * v.y, z * v.z, w * v.w); } + vec_t vec_t::Abs() const { return makeVect(fabsf(x), fabsf(y), fabsf(z)); } + + vec_t Normalized(const vec_t& v) { vec_t res; res = v; res.Normalize(); return res; } + vec_t Cross(const vec_t& v1, const vec_t& v2) + { + vec_t res; + res.x = v1.y * v2.z - v1.z * v2.y; + res.y = v1.z * v2.x - v1.x * v2.z; + res.z = v1.x * v2.y - v1.y * v2.x; + res.w = 0.f; + return res; + } + + float Dot(const vec_t& v1, const vec_t& v2) + { + return (v1.x * v2.x) + (v1.y * v2.y) + (v1.z * v2.z); + } + + vec_t BuildPlan(const vec_t& p_point1, const vec_t& p_normal) + { + vec_t normal, res; + normal.Normalize(p_normal); + res.w = normal.Dot(p_point1); + res.x = normal.x; + res.y = normal.y; + res.z = normal.z; + return res; + } + + struct matrix_t + { + public: + + union + { + float m[4][4]; + float m16[16]; + struct + { + vec_t right, up, dir, position; + } v; + vec_t component[4]; + }; + + operator float* () { return m16; } + operator const float* () const { return m16; } + void Translation(float _x, float _y, float _z) { this->Translation(makeVect(_x, _y, _z)); } + + void Translation(const vec_t& vt) + { + v.right.Set(1.f, 0.f, 0.f, 0.f); + v.up.Set(0.f, 1.f, 0.f, 0.f); + v.dir.Set(0.f, 0.f, 1.f, 0.f); + v.position.Set(vt.x, vt.y, vt.z, 1.f); + } + + void Scale(float _x, float _y, float _z) + { + v.right.Set(_x, 0.f, 0.f, 0.f); + v.up.Set(0.f, _y, 0.f, 0.f); + v.dir.Set(0.f, 0.f, _z, 0.f); + v.position.Set(0.f, 0.f, 0.f, 1.f); + } + void Scale(const vec_t& s) { Scale(s.x, s.y, s.z); } + + matrix_t& operator *= (const matrix_t& mat) + { + matrix_t tmpMat; + tmpMat = *this; + tmpMat.Multiply(mat); + *this = tmpMat; + return *this; + } + matrix_t operator * (const matrix_t& mat) const + { + matrix_t matT; + matT.Multiply(*this, mat); + return matT; + } + + void Multiply(const matrix_t& matrix) + { + matrix_t tmp; + tmp = *this; + + FPU_MatrixF_x_MatrixF((float*)&tmp, (float*)&matrix, (float*)this); + } + + void Multiply(const matrix_t& m1, const matrix_t& m2) + { + FPU_MatrixF_x_MatrixF((float*)&m1, (float*)&m2, (float*)this); + } + + float GetDeterminant() const + { + return m[0][0] * m[1][1] * m[2][2] + m[0][1] * m[1][2] * m[2][0] + m[0][2] * m[1][0] * m[2][1] - + m[0][2] * m[1][1] * m[2][0] - m[0][1] * m[1][0] * m[2][2] - m[0][0] * m[1][2] * m[2][1]; + } + + float Inverse(const matrix_t& srcMatrix, bool affine = false); + void SetToIdentity() + { + v.right.Set(1.f, 0.f, 0.f, 0.f); + v.up.Set(0.f, 1.f, 0.f, 0.f); + v.dir.Set(0.f, 0.f, 1.f, 0.f); + v.position.Set(0.f, 0.f, 0.f, 1.f); + } + void Transpose() + { + matrix_t tmpm; + for (int l = 0; l < 4; l++) + { + for (int c = 0; c < 4; c++) + { + tmpm.m[l][c] = m[c][l]; + } + } + (*this) = tmpm; + } + + void RotationAxis(const vec_t& axis, float angle); + + void OrthoNormalize() + { + v.right.Normalize(); + v.up.Normalize(); + v.dir.Normalize(); + } + }; + + void vec_t::Transform(const matrix_t& matrix) + { + vec_t out; + + out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0] + w * matrix.m[3][0]; + out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1] + w * matrix.m[3][1]; + out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2] + w * matrix.m[3][2]; + out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3] + w * matrix.m[3][3]; + + x = out.x; + y = out.y; + z = out.z; + w = out.w; + } + + void vec_t::Transform(const vec_t& s, const matrix_t& matrix) + { + *this = s; + Transform(matrix); + } + + void vec_t::TransformPoint(const matrix_t& matrix) + { + vec_t out; + + out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0] + matrix.m[3][0]; + out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1] + matrix.m[3][1]; + out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2] + matrix.m[3][2]; + out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3] + matrix.m[3][3]; + + x = out.x; + y = out.y; + z = out.z; + w = out.w; + } + + void vec_t::TransformVector(const matrix_t& matrix) + { + vec_t out; + + out.x = x * matrix.m[0][0] + y * matrix.m[1][0] + z * matrix.m[2][0]; + out.y = x * matrix.m[0][1] + y * matrix.m[1][1] + z * matrix.m[2][1]; + out.z = x * matrix.m[0][2] + y * matrix.m[1][2] + z * matrix.m[2][2]; + out.w = x * matrix.m[0][3] + y * matrix.m[1][3] + z * matrix.m[2][3]; + + x = out.x; + y = out.y; + z = out.z; + w = out.w; + } + + float matrix_t::Inverse(const matrix_t& srcMatrix, bool affine) + { + float det = 0; + + if (affine) + { + det = GetDeterminant(); + float s = 1 / det; + m[0][0] = (srcMatrix.m[1][1] * srcMatrix.m[2][2] - srcMatrix.m[1][2] * srcMatrix.m[2][1]) * s; + m[0][1] = (srcMatrix.m[2][1] * srcMatrix.m[0][2] - srcMatrix.m[2][2] * srcMatrix.m[0][1]) * s; + m[0][2] = (srcMatrix.m[0][1] * srcMatrix.m[1][2] - srcMatrix.m[0][2] * srcMatrix.m[1][1]) * s; + m[1][0] = (srcMatrix.m[1][2] * srcMatrix.m[2][0] - srcMatrix.m[1][0] * srcMatrix.m[2][2]) * s; + m[1][1] = (srcMatrix.m[2][2] * srcMatrix.m[0][0] - srcMatrix.m[2][0] * srcMatrix.m[0][2]) * s; + m[1][2] = (srcMatrix.m[0][2] * srcMatrix.m[1][0] - srcMatrix.m[0][0] * srcMatrix.m[1][2]) * s; + m[2][0] = (srcMatrix.m[1][0] * srcMatrix.m[2][1] - srcMatrix.m[1][1] * srcMatrix.m[2][0]) * s; + m[2][1] = (srcMatrix.m[2][0] * srcMatrix.m[0][1] - srcMatrix.m[2][1] * srcMatrix.m[0][0]) * s; + m[2][2] = (srcMatrix.m[0][0] * srcMatrix.m[1][1] - srcMatrix.m[0][1] * srcMatrix.m[1][0]) * s; + m[3][0] = -(m[0][0] * srcMatrix.m[3][0] + m[1][0] * srcMatrix.m[3][1] + m[2][0] * srcMatrix.m[3][2]); + m[3][1] = -(m[0][1] * srcMatrix.m[3][0] + m[1][1] * srcMatrix.m[3][1] + m[2][1] * srcMatrix.m[3][2]); + m[3][2] = -(m[0][2] * srcMatrix.m[3][0] + m[1][2] * srcMatrix.m[3][1] + m[2][2] * srcMatrix.m[3][2]); + } + else + { + // transpose matrix + float src[16]; + for (int i = 0; i < 4; ++i) + { + src[i] = srcMatrix.m16[i * 4]; + src[i + 4] = srcMatrix.m16[i * 4 + 1]; + src[i + 8] = srcMatrix.m16[i * 4 + 2]; + src[i + 12] = srcMatrix.m16[i * 4 + 3]; + } + + // calculate pairs for first 8 elements (cofactors) + float tmp[12]; // temp array for pairs + tmp[0] = src[10] * src[15]; + tmp[1] = src[11] * src[14]; + tmp[2] = src[9] * src[15]; + tmp[3] = src[11] * src[13]; + tmp[4] = src[9] * src[14]; + tmp[5] = src[10] * src[13]; + tmp[6] = src[8] * src[15]; + tmp[7] = src[11] * src[12]; + tmp[8] = src[8] * src[14]; + tmp[9] = src[10] * src[12]; + tmp[10] = src[8] * src[13]; + tmp[11] = src[9] * src[12]; + + // calculate first 8 elements (cofactors) + m16[0] = (tmp[0] * src[5] + tmp[3] * src[6] + tmp[4] * src[7]) - (tmp[1] * src[5] + tmp[2] * src[6] + tmp[5] * src[7]); + m16[1] = (tmp[1] * src[4] + tmp[6] * src[6] + tmp[9] * src[7]) - (tmp[0] * src[4] + tmp[7] * src[6] + tmp[8] * src[7]); + m16[2] = (tmp[2] * src[4] + tmp[7] * src[5] + tmp[10] * src[7]) - (tmp[3] * src[4] + tmp[6] * src[5] + tmp[11] * src[7]); + m16[3] = (tmp[5] * src[4] + tmp[8] * src[5] + tmp[11] * src[6]) - (tmp[4] * src[4] + tmp[9] * src[5] + tmp[10] * src[6]); + m16[4] = (tmp[1] * src[1] + tmp[2] * src[2] + tmp[5] * src[3]) - (tmp[0] * src[1] + tmp[3] * src[2] + tmp[4] * src[3]); + m16[5] = (tmp[0] * src[0] + tmp[7] * src[2] + tmp[8] * src[3]) - (tmp[1] * src[0] + tmp[6] * src[2] + tmp[9] * src[3]); + m16[6] = (tmp[3] * src[0] + tmp[6] * src[1] + tmp[11] * src[3]) - (tmp[2] * src[0] + tmp[7] * src[1] + tmp[10] * src[3]); + m16[7] = (tmp[4] * src[0] + tmp[9] * src[1] + tmp[10] * src[2]) - (tmp[5] * src[0] + tmp[8] * src[1] + tmp[11] * src[2]); + + // calculate pairs for second 8 elements (cofactors) + tmp[0] = src[2] * src[7]; + tmp[1] = src[3] * src[6]; + tmp[2] = src[1] * src[7]; + tmp[3] = src[3] * src[5]; + tmp[4] = src[1] * src[6]; + tmp[5] = src[2] * src[5]; + tmp[6] = src[0] * src[7]; + tmp[7] = src[3] * src[4]; + tmp[8] = src[0] * src[6]; + tmp[9] = src[2] * src[4]; + tmp[10] = src[0] * src[5]; + tmp[11] = src[1] * src[4]; + + // calculate second 8 elements (cofactors) + m16[8] = (tmp[0] * src[13] + tmp[3] * src[14] + tmp[4] * src[15]) - (tmp[1] * src[13] + tmp[2] * src[14] + tmp[5] * src[15]); + m16[9] = (tmp[1] * src[12] + tmp[6] * src[14] + tmp[9] * src[15]) - (tmp[0] * src[12] + tmp[7] * src[14] + tmp[8] * src[15]); + m16[10] = (tmp[2] * src[12] + tmp[7] * src[13] + tmp[10] * src[15]) - (tmp[3] * src[12] + tmp[6] * src[13] + tmp[11] * src[15]); + m16[11] = (tmp[5] * src[12] + tmp[8] * src[13] + tmp[11] * src[14]) - (tmp[4] * src[12] + tmp[9] * src[13] + tmp[10] * src[14]); + m16[12] = (tmp[2] * src[10] + tmp[5] * src[11] + tmp[1] * src[9]) - (tmp[4] * src[11] + tmp[0] * src[9] + tmp[3] * src[10]); + m16[13] = (tmp[8] * src[11] + tmp[0] * src[8] + tmp[7] * src[10]) - (tmp[6] * src[10] + tmp[9] * src[11] + tmp[1] * src[8]); + m16[14] = (tmp[6] * src[9] + tmp[11] * src[11] + tmp[3] * src[8]) - (tmp[10] * src[11] + tmp[2] * src[8] + tmp[7] * src[9]); + m16[15] = (tmp[10] * src[10] + tmp[4] * src[8] + tmp[9] * src[9]) - (tmp[8] * src[9] + tmp[11] * src[10] + tmp[5] * src[8]); + + // calculate determinant + det = src[0] * m16[0] + src[1] * m16[1] + src[2] * m16[2] + src[3] * m16[3]; + + // calculate matrix inverse + float invdet = 1 / det; + for (int j = 0; j < 16; ++j) + { + m16[j] *= invdet; + } + } + + return det; + } + + void matrix_t::RotationAxis(const vec_t& axis, float angle) + { + float length2 = axis.LengthSq(); + if (length2 < FLT_EPSILON) + { + SetToIdentity(); + return; + } + + vec_t n = axis * (1.f / sqrtf(length2)); + float s = sinf(angle); + float c = cosf(angle); + float k = 1.f - c; + + float xx = n.x * n.x * k + c; + float yy = n.y * n.y * k + c; + float zz = n.z * n.z * k + c; + float xy = n.x * n.y * k; + float yz = n.y * n.z * k; + float zx = n.z * n.x * k; + float xs = n.x * s; + float ys = n.y * s; + float zs = n.z * s; + + m[0][0] = xx; + m[0][1] = xy + zs; + m[0][2] = zx - ys; + m[0][3] = 0.f; + m[1][0] = xy - zs; + m[1][1] = yy; + m[1][2] = yz + xs; + m[1][3] = 0.f; + m[2][0] = zx + ys; + m[2][1] = yz - xs; + m[2][2] = zz; + m[2][3] = 0.f; + m[3][0] = 0.f; + m[3][1] = 0.f; + m[3][2] = 0.f; + m[3][3] = 1.f; + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + + enum MOVETYPE + { + MT_NONE, + MT_MOVE_X, + MT_MOVE_Y, + MT_MOVE_Z, + MT_MOVE_YZ, + MT_MOVE_ZX, + MT_MOVE_XY, + MT_MOVE_SCREEN, + MT_ROTATE_X, + MT_ROTATE_Y, + MT_ROTATE_Z, + MT_ROTATE_SCREEN, + MT_SCALE_X, + MT_SCALE_Y, + MT_SCALE_Z, + MT_SCALE_XYZ + }; + + static bool IsTranslateType(int type) + { + return type >= MT_MOVE_X && type <= MT_MOVE_SCREEN; + } + + static bool IsRotateType(int type) + { + return type >= MT_ROTATE_X && type <= MT_ROTATE_SCREEN; + } + + static bool IsScaleType(int type) + { + return type >= MT_SCALE_X && type <= MT_SCALE_XYZ; + } + + // Matches MT_MOVE_AB order + static const OPERATION TRANSLATE_PLANS[3] = { TRANSLATE_Y | TRANSLATE_Z, TRANSLATE_X | TRANSLATE_Z, TRANSLATE_X | TRANSLATE_Y }; + + Style::Style() + { + // default values + TranslationLineThickness = 3.0f; + TranslationLineArrowSize = 6.0f; + RotationLineThickness = 2.0f; + RotationOuterLineThickness = 3.0f; + ScaleLineThickness = 3.0f; + ScaleLineCircleSize = 6.0f; + HatchedAxisLineThickness = 6.0f; + CenterCircleSize = 6.0f; + + // initialize default colors + Colors[DIRECTION_X] = ImVec4(0.666f, 0.000f, 0.000f, 1.000f); + Colors[DIRECTION_Y] = ImVec4(0.000f, 0.666f, 0.000f, 1.000f); + Colors[DIRECTION_Z] = ImVec4(0.000f, 0.000f, 0.666f, 1.000f); + Colors[PLANE_X] = ImVec4(0.666f, 0.000f, 0.000f, 0.380f); + Colors[PLANE_Y] = ImVec4(0.000f, 0.666f, 0.000f, 0.380f); + Colors[PLANE_Z] = ImVec4(0.000f, 0.000f, 0.666f, 0.380f); + Colors[SELECTION] = ImVec4(1.000f, 0.500f, 0.062f, 0.541f); + Colors[INACTIVE] = ImVec4(0.600f, 0.600f, 0.600f, 0.600f); + Colors[TRANSLATION_LINE] = ImVec4(0.666f, 0.666f, 0.666f, 0.666f); + Colors[SCALE_LINE] = ImVec4(0.250f, 0.250f, 0.250f, 1.000f); + Colors[ROTATION_USING_BORDER] = ImVec4(1.000f, 0.500f, 0.062f, 1.000f); + Colors[ROTATION_USING_FILL] = ImVec4(1.000f, 0.500f, 0.062f, 0.500f); + Colors[HATCHED_AXIS_LINES] = ImVec4(0.000f, 0.000f, 0.000f, 0.500f); + Colors[TEXT] = ImVec4(1.000f, 1.000f, 1.000f, 1.000f); + Colors[TEXT_SHADOW] = ImVec4(0.000f, 0.000f, 0.000f, 1.000f); + } + + struct Context + { + Context() : mbUsing(false), mbUsingViewManipulate(false), mbEnable(true), mIsViewManipulatorHovered(false), mbUsingBounds(false) + { + } + + ImDrawList* mDrawList; + Style mStyle; + + MODE mMode; + matrix_t mViewMat; + matrix_t mProjectionMat; + matrix_t mModel; + matrix_t mModelLocal; // orthonormalized model + matrix_t mModelInverse; + matrix_t mModelSource; + matrix_t mModelSourceInverse; + matrix_t mMVP; + matrix_t mMVPLocal; // MVP with full model matrix whereas mMVP's model matrix might only be translation in case of World space edition + matrix_t mViewProjection; + + vec_t mModelScaleOrigin; + vec_t mCameraEye; + vec_t mCameraRight; + vec_t mCameraDir; + vec_t mCameraUp; + vec_t mRayOrigin; + vec_t mRayVector; + + float mRadiusSquareCenter; + ImVec2 mScreenSquareCenter; + ImVec2 mScreenSquareMin; + ImVec2 mScreenSquareMax; + + float mScreenFactor; + vec_t mRelativeOrigin; + + bool mbUsing; + bool mbUsingViewManipulate; + bool mbEnable; + bool mbMouseOver; + bool mReversed; // reversed projection matrix + bool mIsViewManipulatorHovered; + + // translation + vec_t mTranslationPlan; + vec_t mTranslationPlanOrigin; + vec_t mMatrixOrigin; + vec_t mTranslationLastDelta; + + // rotation + vec_t mRotationVectorSource; + float mRotationAngle; + float mRotationAngleOrigin; + //vec_t mWorldToLocalAxis; + + // scale + vec_t mScale; + vec_t mScaleValueOrigin; + vec_t mScaleLast; + float mSaveMousePosx; + + // save axis factor when using gizmo + bool mBelowAxisLimit[3]; + int mAxisMask = 0; + bool mBelowPlaneLimit[3]; + float mAxisFactor[3]; + + float mAxisLimit=0.0025f; + float mPlaneLimit=0.02f; + + // bounds stretching + vec_t mBoundsPivot; + vec_t mBoundsAnchor; + vec_t mBoundsPlan; + vec_t mBoundsLocalPivot; + int mBoundsBestAxis; + int mBoundsAxis[2]; + bool mbUsingBounds; + matrix_t mBoundsMatrix; + + // + int mCurrentOperation; + + float mX = 0.f; + float mY = 0.f; + float mWidth = 0.f; + float mHeight = 0.f; + float mXMax = 0.f; + float mYMax = 0.f; + float mDisplayRatio = 1.f; + + bool mIsOrthographic = false; + // check to not have multiple gizmo highlighted at the same time + bool mbOverGizmoHotspot = false; + + ImGuiWindow* mAlternativeWindow = nullptr; + ImVector mIDStack; + ImGuiID mEditingID = -1; + OPERATION mOperation = OPERATION(-1); + + bool mAllowAxisFlip = true; + float mGizmoSizeClipSpace = 0.1f; + + inline ImGuiID GetCurrentID() + { + if (mIDStack.empty()) + { + mIDStack.push_back(-1); + } + return mIDStack.back(); + } + }; + + static Context gContext; + + static const vec_t directionUnary[3] = { makeVect(1.f, 0.f, 0.f), makeVect(0.f, 1.f, 0.f), makeVect(0.f, 0.f, 1.f) }; + static const char* translationInfoMask[] = { "X : %5.3f", "Y : %5.3f", "Z : %5.3f", + "Y : %5.3f Z : %5.3f", "X : %5.3f Z : %5.3f", "X : %5.3f Y : %5.3f", + "X : %5.3f Y : %5.3f Z : %5.3f" }; + static const char* scaleInfoMask[] = { "X : %5.2f", "Y : %5.2f", "Z : %5.2f", "XYZ : %5.2f" }; + static const char* rotationInfoMask[] = { "X : %5.2f deg %5.2f rad", "Y : %5.2f deg %5.2f rad", "Z : %5.2f deg %5.2f rad", "Screen : %5.2f deg %5.2f rad" }; + static const int translationInfoIndex[] = { 0,0,0, 1,0,0, 2,0,0, 1,2,0, 0,2,0, 0,1,0, 0,1,2 }; + static const float quadMin = 0.5f; + static const float quadMax = 0.8f; + static const float quadUV[8] = { quadMin, quadMin, quadMin, quadMax, quadMax, quadMax, quadMax, quadMin }; + static const int halfCircleSegmentCount = 64; + static const float snapTension = 0.5f; + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + static int GetMoveType(OPERATION op, vec_t* gizmoHitProportion); + static int GetRotateType(OPERATION op); + static int GetScaleType(OPERATION op); + + Style& GetStyle() + { + return gContext.mStyle; + } + + static ImU32 GetColorU32(int idx) + { + IM_ASSERT(idx < COLOR::COUNT); + return ImGui::ColorConvertFloat4ToU32(gContext.mStyle.Colors[idx]); + } + + static ImVec2 worldToPos(const vec_t& worldPos, const matrix_t& mat, ImVec2 position = ImVec2(gContext.mX, gContext.mY), ImVec2 size = ImVec2(gContext.mWidth, gContext.mHeight)) + { + vec_t trans; + trans.TransformPoint(worldPos, mat); + trans *= 0.5f / trans.w; + trans += makeVect(0.5f, 0.5f); + trans.y = 1.f - trans.y; + trans.x *= size.x; + trans.y *= size.y; + trans.x += position.x; + trans.y += position.y; + return ImVec2(trans.x, trans.y); + } + + static void ComputeCameraRay(vec_t& rayOrigin, vec_t& rayDir, ImVec2 position = ImVec2(gContext.mX, gContext.mY), ImVec2 size = ImVec2(gContext.mWidth, gContext.mHeight)) + { + ImGuiIO& io = ImGui::GetIO(); + + matrix_t mViewProjInverse; + mViewProjInverse.Inverse(gContext.mViewMat * gContext.mProjectionMat); + + const float mox = ((io.MousePos.x - position.x) / size.x) * 2.f - 1.f; + const float moy = (1.f - ((io.MousePos.y - position.y) / size.y)) * 2.f - 1.f; + + const float zNear = gContext.mReversed ? (1.f - FLT_EPSILON) : 0.f; + const float zFar = gContext.mReversed ? 0.f : (1.f - FLT_EPSILON); + + rayOrigin.Transform(makeVect(mox, moy, zNear, 1.f), mViewProjInverse); + rayOrigin *= 1.f / rayOrigin.w; + vec_t rayEnd; + rayEnd.Transform(makeVect(mox, moy, zFar, 1.f), mViewProjInverse); + rayEnd *= 1.f / rayEnd.w; + rayDir = Normalized(rayEnd - rayOrigin); + } + + static float GetSegmentLengthClipSpace(const vec_t& start, const vec_t& end, const bool localCoordinates = false) + { + vec_t startOfSegment = start; + const matrix_t& mvp = localCoordinates ? gContext.mMVPLocal : gContext.mMVP; + startOfSegment.TransformPoint(mvp); + if (fabsf(startOfSegment.w) > FLT_EPSILON) // check for axis aligned with camera direction + { + startOfSegment *= 1.f / startOfSegment.w; + } + + vec_t endOfSegment = end; + endOfSegment.TransformPoint(mvp); + if (fabsf(endOfSegment.w) > FLT_EPSILON) // check for axis aligned with camera direction + { + endOfSegment *= 1.f / endOfSegment.w; + } + + vec_t clipSpaceAxis = endOfSegment - startOfSegment; + if (gContext.mDisplayRatio < 1.0) + clipSpaceAxis.x *= gContext.mDisplayRatio; + else + clipSpaceAxis.y /= gContext.mDisplayRatio; + float segmentLengthInClipSpace = sqrtf(clipSpaceAxis.x * clipSpaceAxis.x + clipSpaceAxis.y * clipSpaceAxis.y); + return segmentLengthInClipSpace; + } + + static float GetParallelogram(const vec_t& ptO, const vec_t& ptA, const vec_t& ptB) + { + vec_t pts[] = { ptO, ptA, ptB }; + for (unsigned int i = 0; i < 3; i++) + { + pts[i].TransformPoint(gContext.mMVP); + if (fabsf(pts[i].w) > FLT_EPSILON) // check for axis aligned with camera direction + { + pts[i] *= 1.f / pts[i].w; + } + } + vec_t segA = pts[1] - pts[0]; + vec_t segB = pts[2] - pts[0]; + segA.y /= gContext.mDisplayRatio; + segB.y /= gContext.mDisplayRatio; + vec_t segAOrtho = makeVect(-segA.y, segA.x); + segAOrtho.Normalize(); + float dt = segAOrtho.Dot3(segB); + float surface = sqrtf(segA.x * segA.x + segA.y * segA.y) * fabsf(dt); + return surface; + } + + inline vec_t PointOnSegment(const vec_t& point, const vec_t& vertPos1, const vec_t& vertPos2) + { + vec_t c = point - vertPos1; + vec_t V; + + V.Normalize(vertPos2 - vertPos1); + float d = (vertPos2 - vertPos1).Length(); + float t = V.Dot3(c); + + if (t < 0.f) + { + return vertPos1; + } + + if (t > d) + { + return vertPos2; + } + + return vertPos1 + V * t; + } + + static float IntersectRayPlane(const vec_t& rOrigin, const vec_t& rVector, const vec_t& plan) + { + const float numer = plan.Dot3(rOrigin) - plan.w; + const float denom = plan.Dot3(rVector); + + if (fabsf(denom) < FLT_EPSILON) // normal is orthogonal to vector, cant intersect + { + return -1.0f; + } + + return -(numer / denom); + } + + static float DistanceToPlane(const vec_t& point, const vec_t& plan) + { + return plan.Dot3(point) + plan.w; + } + + static bool IsInContextRect(ImVec2 p) + { + return IsWithin(p.x, gContext.mX, gContext.mXMax) && IsWithin(p.y, gContext.mY, gContext.mYMax); + } + + static bool IsHoveringWindow() + { + ImGuiContext& g = *ImGui::GetCurrentContext(); + ImGuiWindow* window = ImGui::FindWindowByName(gContext.mDrawList->_OwnerName); + if (g.HoveredWindow == window) // Mouse hovering drawlist window + return true; + if (gContext.mAlternativeWindow != nullptr && g.HoveredWindow == gContext.mAlternativeWindow) + return true; + if (g.HoveredWindow != NULL) // Any other window is hovered + return false; + if (ImGui::IsMouseHoveringRect(window->InnerRect.Min, window->InnerRect.Max, false)) // Hovering drawlist window rect, while no other window is hovered (for _NoInputs windows) + return true; + return false; + } + + void SetRect(float x, float y, float width, float height) + { + gContext.mX = x; + gContext.mY = y; + gContext.mWidth = width; + gContext.mHeight = height; + gContext.mXMax = gContext.mX + gContext.mWidth; + gContext.mYMax = gContext.mY + gContext.mXMax; + gContext.mDisplayRatio = width / height; + } + + void SetOrthographic(bool isOrthographic) + { + gContext.mIsOrthographic = isOrthographic; + } + + void SetDrawlist(ImDrawList* drawlist) + { + gContext.mDrawList = drawlist ? drawlist : ImGui::GetWindowDrawList(); + } + + void SetImGuiContext(ImGuiContext* ctx) + { + ImGui::SetCurrentContext(ctx); + } + + void BeginFrame() + { + const ImU32 flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoBringToFrontOnFocus; #ifdef IMGUI_HAS_VIEWPORT - ImGui::SetNextWindowSize(ImGui::GetMainViewport()->Size); - ImGui::SetNextWindowPos(ImGui::GetMainViewport()->Pos); + ImGui::SetNextWindowSize(ImGui::GetMainViewport()->Size); + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->Pos); #else - ImGuiIO& io = ImGui::GetIO(); - ImGui::SetNextWindowSize(io.DisplaySize); - ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGuiIO& io = ImGui::GetIO(); + ImGui::SetNextWindowSize(io.DisplaySize); + ImGui::SetNextWindowPos(ImVec2(0, 0)); #endif - ImGui::PushStyleColor(ImGuiCol_WindowBg, 0); - ImGui::PushStyleColor(ImGuiCol_Border, 0); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - - ImGui::Begin("gizmo", NULL, flags); - gContext.mDrawList = ImGui::GetWindowDrawList(); - gContext.mbOverGizmoHotspot = false; - ImGui::End(); - ImGui::PopStyleVar(); - ImGui::PopStyleColor(2); - } - - bool IsUsing() - { - return (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) || gContext.mbUsingBounds; - } - - bool IsUsingViewManipulate() - { - return gContext.mbUsingViewManipulate; - } - - bool IsViewManipulateHovered() - { - return gContext.mIsViewManipulatorHovered; - } - - bool IsUsingAny() - { - return gContext.mbUsing || gContext.mbUsingBounds; - } - - bool IsOver() - { - return (Intersects(gContext.mOperation, TRANSLATE) && GetMoveType(gContext.mOperation, NULL) != MT_NONE) || - (Intersects(gContext.mOperation, ROTATE) && GetRotateType(gContext.mOperation) != MT_NONE) || - (Intersects(gContext.mOperation, SCALE) && GetScaleType(gContext.mOperation) != MT_NONE) || IsUsing(); - } - - bool IsOver(OPERATION op) - { - if (IsUsing()) - { - return true; - } - if (Intersects(op, SCALE) && GetScaleType(op) != MT_NONE) - { - return true; - } - if (Intersects(op, ROTATE) && GetRotateType(op) != MT_NONE) - { - return true; - } - if (Intersects(op, TRANSLATE) && GetMoveType(op, NULL) != MT_NONE) - { - return true; - } - return false; - } - - void Enable(bool enable) - { - gContext.mbEnable = enable; - if (!enable) - { - gContext.mbUsing = false; - gContext.mbUsingBounds = false; - } - } - - static void ComputeContext(const float* view, const float* projection, float* matrix, MODE mode) - { - gContext.mMode = mode; - gContext.mViewMat = *(matrix_t*)view; - gContext.mProjectionMat = *(matrix_t*)projection; - gContext.mbMouseOver = IsHoveringWindow(); - - gContext.mModelLocal = *(matrix_t*)matrix; - gContext.mModelLocal.OrthoNormalize(); - - if (mode == LOCAL) - { - gContext.mModel = gContext.mModelLocal; - } - else - { - gContext.mModel.Translation(((matrix_t*)matrix)->v.position); - } - gContext.mModelSource = *(matrix_t*)matrix; - gContext.mModelScaleOrigin.Set( - gContext.mModelSource.v.right.Length(), gContext.mModelSource.v.up.Length(), gContext.mModelSource.v.dir.Length()); - - gContext.mModelInverse.Inverse(gContext.mModel); - gContext.mModelSourceInverse.Inverse(gContext.mModelSource); - gContext.mViewProjection = gContext.mViewMat * gContext.mProjectionMat; - gContext.mMVP = gContext.mModel * gContext.mViewProjection; - gContext.mMVPLocal = gContext.mModelLocal * gContext.mViewProjection; - - matrix_t viewInverse; - viewInverse.Inverse(gContext.mViewMat); - gContext.mCameraDir = viewInverse.v.dir; - gContext.mCameraEye = viewInverse.v.position; - gContext.mCameraRight = viewInverse.v.right; - gContext.mCameraUp = viewInverse.v.up; - - // projection reverse - vec_t nearPos, farPos; - nearPos.Transform(makeVect(0, 0, 1.f, 1.f), gContext.mProjectionMat); - farPos.Transform(makeVect(0, 0, 2.f, 1.f), gContext.mProjectionMat); - - gContext.mReversed = (nearPos.z / nearPos.w) > (farPos.z / farPos.w); - - // compute scale from the size of camera right vector projected on screen at the matrix position - vec_t pointRight = viewInverse.v.right; - pointRight.TransformPoint(gContext.mViewProjection); - - vec_t rightViewInverse = viewInverse.v.right; - rightViewInverse.TransformVector(gContext.mModelInverse); - float rightLength = GetSegmentLengthClipSpace(makeVect(0.f, 0.f), rightViewInverse); - gContext.mScreenFactor = gContext.mGizmoSizeClipSpace / rightLength; - - ImVec2 centerSSpace = worldToPos(makeVect(0.f, 0.f), gContext.mMVP); - gContext.mScreenSquareCenter = centerSSpace; - gContext.mScreenSquareMin = ImVec2(centerSSpace.x - 10.f, centerSSpace.y - 10.f); - gContext.mScreenSquareMax = ImVec2(centerSSpace.x + 10.f, centerSSpace.y + 10.f); - - ComputeCameraRay(gContext.mRayOrigin, gContext.mRayVector); - } - - static void ComputeColors(ImU32* colors, int type, OPERATION operation) - { - if (gContext.mbEnable) - { - ImU32 selectionColor = GetColorU32(SELECTION); - - switch (operation) + ImGui::PushStyleColor(ImGuiCol_WindowBg, 0); + ImGui::PushStyleColor(ImGuiCol_Border, 0); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + + ImGui::Begin("gizmo", NULL, flags); + gContext.mDrawList = ImGui::GetWindowDrawList(); + gContext.mbOverGizmoHotspot = false; + ImGui::End(); + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); + } + + bool IsUsing() + { + return (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) || gContext.mbUsingBounds; + } + + bool IsUsingViewManipulate() + { + return gContext.mbUsingViewManipulate; + } + + bool IsViewManipulateHovered() + { + return gContext.mIsViewManipulatorHovered; + } + + bool IsUsingAny() + { + return gContext.mbUsing || gContext.mbUsingBounds; + } + + bool IsOver() + { + return (Intersects(gContext.mOperation, TRANSLATE) && GetMoveType(gContext.mOperation, NULL) != MT_NONE) || + (Intersects(gContext.mOperation, ROTATE) && GetRotateType(gContext.mOperation) != MT_NONE) || + (Intersects(gContext.mOperation, SCALE) && GetScaleType(gContext.mOperation) != MT_NONE) || IsUsing(); + } + + bool IsOver(OPERATION op) + { + if(IsUsing()) + { + return true; + } + if(Intersects(op, SCALE) && GetScaleType(op) != MT_NONE) + { + return true; + } + if(Intersects(op, ROTATE) && GetRotateType(op) != MT_NONE) + { + return true; + } + if(Intersects(op, TRANSLATE) && GetMoveType(op, NULL) != MT_NONE) + { + return true; + } + return false; + } + + void Enable(bool enable) + { + gContext.mbEnable = enable; + if (!enable) + { + gContext.mbUsing = false; + gContext.mbUsingBounds = false; + } + } + + static void ComputeContext(const float* view, const float* projection, float* matrix, MODE mode) + { + gContext.mMode = mode; + gContext.mViewMat = *(matrix_t*)view; + gContext.mProjectionMat = *(matrix_t*)projection; + gContext.mbMouseOver = IsHoveringWindow(); + + gContext.mModelLocal = *(matrix_t*)matrix; + gContext.mModelLocal.OrthoNormalize(); + + if (mode == LOCAL) + { + gContext.mModel = gContext.mModelLocal; + } + else + { + gContext.mModel.Translation(((matrix_t*)matrix)->v.position); + } + gContext.mModelSource = *(matrix_t*)matrix; + gContext.mModelScaleOrigin.Set(gContext.mModelSource.v.right.Length(), gContext.mModelSource.v.up.Length(), gContext.mModelSource.v.dir.Length()); + + gContext.mModelInverse.Inverse(gContext.mModel); + gContext.mModelSourceInverse.Inverse(gContext.mModelSource); + gContext.mViewProjection = gContext.mViewMat * gContext.mProjectionMat; + gContext.mMVP = gContext.mModel * gContext.mViewProjection; + gContext.mMVPLocal = gContext.mModelLocal * gContext.mViewProjection; + + matrix_t viewInverse; + viewInverse.Inverse(gContext.mViewMat); + gContext.mCameraDir = viewInverse.v.dir; + gContext.mCameraEye = viewInverse.v.position; + gContext.mCameraRight = viewInverse.v.right; + gContext.mCameraUp = viewInverse.v.up; + + // projection reverse + vec_t nearPos, farPos; + nearPos.Transform(makeVect(0, 0, 1.f, 1.f), gContext.mProjectionMat); + farPos.Transform(makeVect(0, 0, 2.f, 1.f), gContext.mProjectionMat); + + gContext.mReversed = (nearPos.z/nearPos.w) > (farPos.z / farPos.w); + + // compute scale from the size of camera right vector projected on screen at the matrix position + vec_t pointRight = viewInverse.v.right; + pointRight.TransformPoint(gContext.mViewProjection); + + vec_t rightViewInverse = viewInverse.v.right; + rightViewInverse.TransformVector(gContext.mModelInverse); + float rightLength = GetSegmentLengthClipSpace(makeVect(0.f, 0.f), rightViewInverse); + gContext.mScreenFactor = gContext.mGizmoSizeClipSpace / rightLength; + + ImVec2 centerSSpace = worldToPos(makeVect(0.f, 0.f), gContext.mMVP); + gContext.mScreenSquareCenter = centerSSpace; + gContext.mScreenSquareMin = ImVec2(centerSSpace.x - 10.f, centerSSpace.y - 10.f); + gContext.mScreenSquareMax = ImVec2(centerSSpace.x + 10.f, centerSSpace.y + 10.f); + + ComputeCameraRay(gContext.mRayOrigin, gContext.mRayVector); + } + + static void ComputeColors(ImU32* colors, int type, OPERATION operation) + { + if (gContext.mbEnable) + { + ImU32 selectionColor = GetColorU32(SELECTION); + + switch (operation) + { + case TRANSLATE: + colors[0] = (type == MT_MOVE_SCREEN) ? selectionColor : IM_COL32_WHITE; + for (int i = 0; i < 3; i++) { - case TRANSLATE: - colors[0] = (type == MT_MOVE_SCREEN) ? selectionColor : IM_COL32_WHITE; - for (int i = 0; i < 3; i++) - { - colors[i + 1] = (type == (int)(MT_MOVE_X + i)) ? selectionColor : GetColorU32(DIRECTION_X + i); - colors[i + 4] = (type == (int)(MT_MOVE_YZ + i)) ? selectionColor : GetColorU32(PLANE_X + i); - colors[i + 4] = (type == MT_MOVE_SCREEN) ? selectionColor : colors[i + 4]; - } - break; - case ROTATE: - colors[0] = (type == MT_ROTATE_SCREEN) ? selectionColor : IM_COL32_WHITE; - for (int i = 0; i < 3; i++) - { - colors[i + 1] = (type == (int)(MT_ROTATE_X + i)) ? selectionColor : GetColorU32(DIRECTION_X + i); - } - break; - case SCALEU: - case SCALE: - colors[0] = (type == MT_SCALE_XYZ) ? selectionColor : IM_COL32_WHITE; - for (int i = 0; i < 3; i++) - { - colors[i + 1] = (type == (int)(MT_SCALE_X + i)) ? selectionColor : GetColorU32(DIRECTION_X + i); - } - break; - // note: this internal function is only called with three possible values for operation - default: - break; + colors[i + 1] = (type == (int)(MT_MOVE_X + i)) ? selectionColor : GetColorU32(DIRECTION_X + i); + colors[i + 4] = (type == (int)(MT_MOVE_YZ + i)) ? selectionColor : GetColorU32(PLANE_X + i); + colors[i + 4] = (type == MT_MOVE_SCREEN) ? selectionColor : colors[i + 4]; } - } - else - { - ImU32 inactiveColor = GetColorU32(INACTIVE); - for (int i = 0; i < 7; i++) + break; + case ROTATE: + colors[0] = (type == MT_ROTATE_SCREEN) ? selectionColor : IM_COL32_WHITE; + for (int i = 0; i < 3; i++) { - colors[i] = inactiveColor; + colors[i + 1] = (type == (int)(MT_ROTATE_X + i)) ? selectionColor : GetColorU32(DIRECTION_X + i); } - } - } - - static void ComputeTripodAxisAndVisibility( - const int axisIndex, - vec_t& dirAxis, - vec_t& dirPlaneX, - vec_t& dirPlaneY, - bool& belowAxisLimit, - bool& belowPlaneLimit, - const bool localCoordinates = false) - { - dirAxis = directionUnary[axisIndex]; - dirPlaneX = directionUnary[(axisIndex + 1) % 3]; - dirPlaneY = directionUnary[(axisIndex + 2) % 3]; - - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) - { - // when using, use stored factors so the gizmo doesn't flip when we translate - - // Apply axis mask to axes and planes - belowAxisLimit = gContext.mBelowAxisLimit[axisIndex] && ((1 << axisIndex) & gContext.mAxisMask); - belowPlaneLimit = gContext.mBelowPlaneLimit[axisIndex] && (((1 << axisIndex) == gContext.mAxisMask) || !gContext.mAxisMask); - - dirAxis *= gContext.mAxisFactor[axisIndex]; - dirPlaneX *= gContext.mAxisFactor[(axisIndex + 1) % 3]; - dirPlaneY *= gContext.mAxisFactor[(axisIndex + 2) % 3]; - } - else - { - // new method - float lenDir = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirAxis, localCoordinates); - float lenDirMinus = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), -dirAxis, localCoordinates); - - float lenDirPlaneX = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirPlaneX, localCoordinates); - float lenDirMinusPlaneX = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), -dirPlaneX, localCoordinates); - - float lenDirPlaneY = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirPlaneY, localCoordinates); - float lenDirMinusPlaneY = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), -dirPlaneY, localCoordinates); - - // For readability - bool& allowFlip = gContext.mAllowAxisFlip; - float mulAxis = (allowFlip && lenDir < lenDirMinus && fabsf(lenDir - lenDirMinus) > FLT_EPSILON) ? -1.f : 1.f; - float mulAxisX = - (allowFlip && lenDirPlaneX < lenDirMinusPlaneX && fabsf(lenDirPlaneX - lenDirMinusPlaneX) > FLT_EPSILON) ? -1.f : 1.f; - float mulAxisY = - (allowFlip && lenDirPlaneY < lenDirMinusPlaneY && fabsf(lenDirPlaneY - lenDirMinusPlaneY) > FLT_EPSILON) ? -1.f : 1.f; - dirAxis *= mulAxis; - dirPlaneX *= mulAxisX; - dirPlaneY *= mulAxisY; - - // for axis - float axisLengthInClipSpace = - GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirAxis * gContext.mScreenFactor, localCoordinates); - - float paraSurf = - GetParallelogram(makeVect(0.f, 0.f, 0.f), dirPlaneX * gContext.mScreenFactor, dirPlaneY * gContext.mScreenFactor); - // Apply axis mask to axes and planes - belowPlaneLimit = (paraSurf > gContext.mAxisLimit) && (((1 << axisIndex) == gContext.mAxisMask) || !gContext.mAxisMask); - belowAxisLimit = (axisLengthInClipSpace > gContext.mPlaneLimit) && !((1 << axisIndex) & gContext.mAxisMask); - - // and store values - gContext.mAxisFactor[axisIndex] = mulAxis; - gContext.mAxisFactor[(axisIndex + 1) % 3] = mulAxisX; - gContext.mAxisFactor[(axisIndex + 2) % 3] = mulAxisY; - gContext.mBelowAxisLimit[axisIndex] = belowAxisLimit; - gContext.mBelowPlaneLimit[axisIndex] = belowPlaneLimit; - } - } - - static void ComputeSnap(float* value, float snap) - { - if (snap <= FLT_EPSILON) - { - return; - } - - float modulo = fmodf(*value, snap); - float moduloRatio = fabsf(modulo) / snap; - if (moduloRatio < snapTension) - { - *value -= modulo; - } - else if (moduloRatio > (1.f - snapTension)) - { - *value = *value - modulo + snap * ((*value < 0.f) ? -1.f : 1.f); - } - } - static void ComputeSnap(vec_t& value, const float* snap) - { - for (int i = 0; i < 3; i++) - { - ComputeSnap(&value[i], snap[i]); - } - } - - static float ComputeAngleOnPlan() - { - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); - vec_t localPos = Normalized(gContext.mRayOrigin + gContext.mRayVector * len - gContext.mModel.v.position); - - vec_t perpendicularVector; - perpendicularVector.Cross(gContext.mRotationVectorSource, gContext.mTranslationPlan); - perpendicularVector.Normalize(); - float acosAngle = Clamp(Dot(localPos, gContext.mRotationVectorSource), -1.f, 1.f); - float angle = acosf(acosAngle); - angle *= (Dot(localPos, perpendicularVector) < 0.f) ? 1.f : -1.f; - return angle; - } - - static void DrawRotationGizmo(OPERATION op, int type) - { - if (!Intersects(op, ROTATE)) - { - return; - } - ImDrawList* drawList = gContext.mDrawList; - - bool isMultipleAxesMasked = gContext.mAxisMask & (gContext.mAxisMask - 1); - bool isNoAxesMasked = !gContext.mAxisMask; - - // colors - ImU32 colors[7]; - ComputeColors(colors, type, ROTATE); - - vec_t cameraToModelNormalized; - if (gContext.mIsOrthographic) - { - matrix_t viewInverse; - viewInverse.Inverse(*(matrix_t*)&gContext.mViewMat); - cameraToModelNormalized = -viewInverse.v.dir; - } - else - { - cameraToModelNormalized = Normalized(gContext.mModel.v.position - gContext.mCameraEye); - } - - cameraToModelNormalized.TransformVector(gContext.mModelInverse); - - gContext.mRadiusSquareCenter = screenRotateSize * gContext.mHeight; - - bool hasRSC = Intersects(op, ROTATE_SCREEN); - for (int axis = 0; axis < 3; axis++) - { - if (!Intersects(op, static_cast(ROTATE_Z >> axis))) + break; + case SCALEU: + case SCALE: + colors[0] = (type == MT_SCALE_XYZ) ? selectionColor : IM_COL32_WHITE; + for (int i = 0; i < 3; i++) { - continue; - } - - bool isAxisMasked = (1 << (2 - axis)) & gContext.mAxisMask; + colors[i + 1] = (type == (int)(MT_SCALE_X + i)) ? selectionColor : GetColorU32(DIRECTION_X + i); + } + break; + // note: this internal function is only called with three possible values for operation + default: + break; + } + } + else + { + ImU32 inactiveColor = GetColorU32(INACTIVE); + for (int i = 0; i < 7; i++) + { + colors[i] = inactiveColor; + } + } + } + + static void ComputeTripodAxisAndVisibility(const int axisIndex, vec_t& dirAxis, vec_t& dirPlaneX, vec_t& dirPlaneY, bool& belowAxisLimit, bool& belowPlaneLimit, const bool localCoordinates = false) + { + dirAxis = directionUnary[axisIndex]; + dirPlaneX = directionUnary[(axisIndex + 1) % 3]; + dirPlaneY = directionUnary[(axisIndex + 2) % 3]; + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) + { + // when using, use stored factors so the gizmo doesn't flip when we translate + + // Apply axis mask to axes and planes + belowAxisLimit = gContext.mBelowAxisLimit[axisIndex] && ((1< FLT_EPSILON) ? -1.f : 1.f; + float mulAxisX = (allowFlip && lenDirPlaneX < lenDirMinusPlaneX&& fabsf(lenDirPlaneX - lenDirMinusPlaneX) > FLT_EPSILON) ? -1.f : 1.f; + float mulAxisY = (allowFlip && lenDirPlaneY < lenDirMinusPlaneY&& fabsf(lenDirPlaneY - lenDirMinusPlaneY) > FLT_EPSILON) ? -1.f : 1.f; + dirAxis *= mulAxis; + dirPlaneX *= mulAxisX; + dirPlaneY *= mulAxisY; + + // for axis + float axisLengthInClipSpace = GetSegmentLengthClipSpace(makeVect(0.f, 0.f, 0.f), dirAxis * gContext.mScreenFactor, localCoordinates); + + float paraSurf = GetParallelogram(makeVect(0.f, 0.f, 0.f), dirPlaneX * gContext.mScreenFactor, dirPlaneY * gContext.mScreenFactor); + // Apply axis mask to axes and planes + belowPlaneLimit = (paraSurf > gContext.mAxisLimit) && (((1< gContext.mPlaneLimit) && !((1< (1.f - snapTension)) + { + *value = *value - modulo + snap * ((*value < 0.f) ? -1.f : 1.f); + } + } + static void ComputeSnap(vec_t& value, const float* snap) + { + for (int i = 0; i < 3; i++) + { + ComputeSnap(&value[i], snap[i]); + } + } + + static float ComputeAngleOnPlan() + { + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + vec_t localPos = Normalized(gContext.mRayOrigin + gContext.mRayVector * len - gContext.mModel.v.position); + + vec_t perpendicularVector; + perpendicularVector.Cross(gContext.mRotationVectorSource, gContext.mTranslationPlan); + perpendicularVector.Normalize(); + float acosAngle = Clamp(Dot(localPos, gContext.mRotationVectorSource), -1.f, 1.f); + float angle = acosf(acosAngle); + angle *= (Dot(localPos, perpendicularVector) < 0.f) ? 1.f : -1.f; + return angle; + } + + static void DrawRotationGizmo(OPERATION op, int type) + { + if(!Intersects(op, ROTATE)) + { + return; + } + ImDrawList* drawList = gContext.mDrawList; + + bool isMultipleAxesMasked = gContext.mAxisMask & (gContext.mAxisMask - 1); + bool isNoAxesMasked = !gContext.mAxisMask; + + // colors + ImU32 colors[7]; + ComputeColors(colors, type, ROTATE); + + vec_t cameraToModelNormalized; + if (gContext.mIsOrthographic) + { + matrix_t viewInverse; + viewInverse.Inverse(*(matrix_t*)&gContext.mViewMat); + cameraToModelNormalized = -viewInverse.v.dir; + } + else + { + cameraToModelNormalized = Normalized(gContext.mModel.v.position - gContext.mCameraEye); + } + + cameraToModelNormalized.TransformVector(gContext.mModelInverse); + + gContext.mRadiusSquareCenter = screenRotateSize * gContext.mHeight; + + bool hasRSC = Intersects(op, ROTATE_SCREEN); + for (int axis = 0; axis < 3; axis++) + { + if(!Intersects(op, static_cast(ROTATE_Z >> axis))) + { + continue; + } + + bool isAxisMasked = (1 << (2 - axis)) & gContext.mAxisMask; + + if ((!isAxisMasked || isMultipleAxesMasked) && !isNoAxesMasked) + { + continue; + } + const bool usingAxis = (gContext.mbUsing && type == MT_ROTATE_Z - axis); + const int circleMul = (hasRSC && !usingAxis) ? 1 : 2; + + ImVec2* circlePos = (ImVec2*)alloca(sizeof(ImVec2) * (circleMul * halfCircleSegmentCount + 1)); + + float angleStart = atan2f(cameraToModelNormalized[(4 - axis) % 3], cameraToModelNormalized[(3 - axis) % 3]) + ZPI * 0.5f; + + for (int i = 0; i < circleMul * halfCircleSegmentCount + 1; i++) + { + float ng = angleStart + (float)circleMul * ZPI * ((float)i / (float)(circleMul * halfCircleSegmentCount)); + vec_t axisPos = makeVect(cosf(ng), sinf(ng), 0.f); + vec_t pos = makeVect(axisPos[axis], axisPos[(axis + 1) % 3], axisPos[(axis + 2) % 3]) * gContext.mScreenFactor * rotationDisplayFactor; + circlePos[i] = worldToPos(pos, gContext.mMVP); + } + if (!gContext.mbUsing || usingAxis) + { + drawList->AddPolyline(circlePos, circleMul* halfCircleSegmentCount + 1, colors[3 - axis], false, gContext.mStyle.RotationLineThickness); + } + + float radiusAxis = sqrtf((ImLengthSqr(worldToPos(gContext.mModel.v.position, gContext.mViewProjection) - circlePos[0]))); + if (radiusAxis > gContext.mRadiusSquareCenter) + { + gContext.mRadiusSquareCenter = radiusAxis; + } + } + if(hasRSC && (!gContext.mbUsing || type == MT_ROTATE_SCREEN) && (!isMultipleAxesMasked && isNoAxesMasked)) + { + drawList->AddCircle(worldToPos(gContext.mModel.v.position, gContext.mViewProjection), gContext.mRadiusSquareCenter, colors[0], 64, gContext.mStyle.RotationOuterLineThickness); + } + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsRotateType(type)) + { + ImVec2 circlePos[halfCircleSegmentCount + 1]; + + circlePos[0] = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + for (unsigned int i = 1; i < halfCircleSegmentCount + 1; i++) + { + float ng = gContext.mRotationAngle * ((float)(i - 1) / (float)(halfCircleSegmentCount - 1)); + matrix_t rotateVectorMatrix; + rotateVectorMatrix.RotationAxis(gContext.mTranslationPlan, ng); + vec_t pos; + pos.TransformPoint(gContext.mRotationVectorSource, rotateVectorMatrix); + pos *= gContext.mScreenFactor * rotationDisplayFactor; + circlePos[i] = worldToPos(pos + gContext.mModel.v.position, gContext.mViewProjection); + } + drawList->AddConvexPolyFilled(circlePos, halfCircleSegmentCount + 1, GetColorU32(ROTATION_USING_FILL)); + drawList->AddPolyline(circlePos, halfCircleSegmentCount + 1, GetColorU32(ROTATION_USING_BORDER), true, gContext.mStyle.RotationLineThickness); + + ImVec2 destinationPosOnScreen = circlePos[1]; + char tmps[512]; + ImFormatString(tmps, sizeof(tmps), rotationInfoMask[type - MT_ROTATE_X], (gContext.mRotationAngle / ZPI) * 180.f, gContext.mRotationAngle); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); + } + } + + static void DrawHatchedAxis(const vec_t& axis) + { + if (gContext.mStyle.HatchedAxisLineThickness <= 0.0f) + { + return; + } + + for (int j = 1; j < 10; j++) + { + ImVec2 baseSSpace2 = worldToPos(axis * 0.05f * (float)(j * 2) * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace2 = worldToPos(axis * 0.05f * (float)(j * 2 + 1) * gContext.mScreenFactor, gContext.mMVP); + gContext.mDrawList->AddLine(baseSSpace2, worldDirSSpace2, GetColorU32(HATCHED_AXIS_LINES), gContext.mStyle.HatchedAxisLineThickness); + } + } + + static void DrawScaleGizmo(OPERATION op, int type) + { + ImDrawList* drawList = gContext.mDrawList; + + if(!Intersects(op, SCALE)) + { + return; + } + + // colors + ImU32 colors[7]; + ComputeColors(colors, type, SCALE); + + // draw + vec_t scaleDisplay = { 1.f, 1.f, 1.f, 1.f }; + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) + { + scaleDisplay = gContext.mScale; + } + + for (int i = 0; i < 3; i++) + { + if(!Intersects(op, static_cast(SCALE_X << i))) + { + continue; + } + const bool usingAxis = (gContext.mbUsing && type == MT_SCALE_X + i); + if (!gContext.mbUsing || usingAxis) + { + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); - if ((!isAxisMasked || isMultipleAxesMasked) && !isNoAxesMasked) + // draw axis + if (belowAxisLimit) { - continue; - } - const bool usingAxis = (gContext.mbUsing && type == MT_ROTATE_Z - axis); - const int circleMul = (hasRSC && !usingAxis) ? 1 : 2; - - ImVec2* circlePos = (ImVec2*)alloca(sizeof(ImVec2) * (circleMul * halfCircleSegmentCount + 1)); + bool hasTranslateOnAxis = Contains(op, static_cast(TRANSLATE_X << i)); + float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f; + ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace = worldToPos((dirAxis * markerScale * scaleDisplay[i]) * gContext.mScreenFactor, gContext.mMVP); - float angleStart = atan2f(cameraToModelNormalized[(4 - axis) % 3], cameraToModelNormalized[(3 - axis) % 3]) + ZPI * 0.5f; - - for (int i = 0; i < circleMul * halfCircleSegmentCount + 1; i++) - { - float ng = angleStart + (float)circleMul * ZPI * ((float)i / (float)(circleMul * halfCircleSegmentCount)); - vec_t axisPos = makeVect(cosf(ng), sinf(ng), 0.f); - vec_t pos = makeVect(axisPos[axis], axisPos[(axis + 1) % 3], axisPos[(axis + 2) % 3]) * gContext.mScreenFactor * - rotationDisplayFactor; - circlePos[i] = worldToPos(pos, gContext.mMVP); - } - if (!gContext.mbUsing || usingAxis) - { - drawList->AddPolyline( - circlePos, circleMul * halfCircleSegmentCount + 1, colors[3 - axis], false, gContext.mStyle.RotationLineThickness); - } + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) + { + ImU32 scaleLineColor = GetColorU32(SCALE_LINE); + drawList->AddLine(baseSSpace, worldDirSSpaceNoScale, scaleLineColor, gContext.mStyle.ScaleLineThickness); + drawList->AddCircleFilled(worldDirSSpaceNoScale, gContext.mStyle.ScaleLineCircleSize, scaleLineColor); + } - float radiusAxis = sqrtf((ImLengthSqr(worldToPos(gContext.mModel.v.position, gContext.mViewProjection) - circlePos[0]))); - if (radiusAxis > gContext.mRadiusSquareCenter) - { - gContext.mRadiusSquareCenter = radiusAxis; - } - } - if (hasRSC && (!gContext.mbUsing || type == MT_ROTATE_SCREEN) && (!isMultipleAxesMasked && isNoAxesMasked)) - { - drawList->AddCircle( - worldToPos(gContext.mModel.v.position, gContext.mViewProjection), - gContext.mRadiusSquareCenter, - colors[0], - 64, - gContext.mStyle.RotationOuterLineThickness); - } - - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsRotateType(type)) - { - ImVec2 circlePos[halfCircleSegmentCount + 1]; - - circlePos[0] = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); - for (unsigned int i = 1; i < halfCircleSegmentCount + 1; i++) - { - float ng = gContext.mRotationAngle * ((float)(i - 1) / (float)(halfCircleSegmentCount - 1)); - matrix_t rotateVectorMatrix; - rotateVectorMatrix.RotationAxis(gContext.mTranslationPlan, ng); - vec_t pos; - pos.TransformPoint(gContext.mRotationVectorSource, rotateVectorMatrix); - pos *= gContext.mScreenFactor * rotationDisplayFactor; - circlePos[i] = worldToPos(pos + gContext.mModel.v.position, gContext.mViewProjection); - } - drawList->AddConvexPolyFilled(circlePos, halfCircleSegmentCount + 1, GetColorU32(ROTATION_USING_FILL)); - drawList->AddPolyline( - circlePos, halfCircleSegmentCount + 1, GetColorU32(ROTATION_USING_BORDER), true, gContext.mStyle.RotationLineThickness); + if (!hasTranslateOnAxis || gContext.mbUsing) + { + drawList->AddLine(baseSSpace, worldDirSSpace, colors[i + 1], gContext.mStyle.ScaleLineThickness); + } + drawList->AddCircleFilled(worldDirSSpace, gContext.mStyle.ScaleLineCircleSize, colors[i + 1]); - ImVec2 destinationPosOnScreen = circlePos[1]; - char tmps[512]; - ImFormatString( - tmps, sizeof(tmps), rotationInfoMask[type - MT_ROTATE_X], (gContext.mRotationAngle / ZPI) * 180.f, gContext.mRotationAngle); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); - } - } - - static void DrawHatchedAxis(const vec_t& axis) - { - if (gContext.mStyle.HatchedAxisLineThickness <= 0.0f) - { - return; - } - - for (int j = 1; j < 10; j++) - { - ImVec2 baseSSpace2 = worldToPos(axis * 0.05f * (float)(j * 2) * gContext.mScreenFactor, gContext.mMVP); - ImVec2 worldDirSSpace2 = worldToPos(axis * 0.05f * (float)(j * 2 + 1) * gContext.mScreenFactor, gContext.mMVP); - gContext.mDrawList->AddLine( - baseSSpace2, worldDirSSpace2, GetColorU32(HATCHED_AXIS_LINES), gContext.mStyle.HatchedAxisLineThickness); - } - } - - static void DrawScaleGizmo(OPERATION op, int type) - { - ImDrawList* drawList = gContext.mDrawList; - - if (!Intersects(op, SCALE)) - { - return; - } - - // colors - ImU32 colors[7]; - ComputeColors(colors, type, SCALE); - - // draw - vec_t scaleDisplay = { 1.f, 1.f, 1.f, 1.f }; - - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) - { - scaleDisplay = gContext.mScale; - } - - for (int i = 0; i < 3; i++) - { - if (!Intersects(op, static_cast(SCALE_X << i))) - { - continue; - } - const bool usingAxis = (gContext.mbUsing && type == MT_SCALE_X + i); - if (!gContext.mbUsing || usingAxis) - { - vec_t dirPlaneX, dirPlaneY, dirAxis; - bool belowAxisLimit, belowPlaneLimit; - ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); - - // draw axis - if (belowAxisLimit) - { - bool hasTranslateOnAxis = Contains(op, static_cast(TRANSLATE_X << i)); - float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f; - ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVP); - ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP); - ImVec2 worldDirSSpace = worldToPos((dirAxis * markerScale * scaleDisplay[i]) * gContext.mScreenFactor, gContext.mMVP); - - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) - { - ImU32 scaleLineColor = GetColorU32(SCALE_LINE); - drawList->AddLine(baseSSpace, worldDirSSpaceNoScale, scaleLineColor, gContext.mStyle.ScaleLineThickness); - drawList->AddCircleFilled(worldDirSSpaceNoScale, gContext.mStyle.ScaleLineCircleSize, scaleLineColor); - } - - if (!hasTranslateOnAxis || gContext.mbUsing) - { - drawList->AddLine(baseSSpace, worldDirSSpace, colors[i + 1], gContext.mStyle.ScaleLineThickness); - } - drawList->AddCircleFilled(worldDirSSpace, gContext.mStyle.ScaleLineCircleSize, colors[i + 1]); - - if (gContext.mAxisFactor[i] < 0.f) - { - DrawHatchedAxis(dirAxis * scaleDisplay[i]); - } - } + if (gContext.mAxisFactor[i] < 0.f) + { + DrawHatchedAxis(dirAxis * scaleDisplay[i]); + } } - } - - // draw screen cirle - drawList->AddCircleFilled(gContext.mScreenSquareCenter, gContext.mStyle.CenterCircleSize, colors[0], 32); - - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsScaleType(type)) - { - // ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection); - ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); - /*vec_t dif(destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y); - dif.Normalize(); - dif *= 5.f; - drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor); - drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor); - drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, - destinationPosOnScreen.y - dif.y), translationLineColor, 2.f); - */ - char tmps[512]; - // vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin; - int componentInfoIndex = (type - MT_SCALE_X) * 3; - ImFormatString(tmps, sizeof(tmps), scaleInfoMask[type - MT_SCALE_X], scaleDisplay[translationInfoIndex[componentInfoIndex]]); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); - } - } - - static void DrawScaleUniveralGizmo(OPERATION op, int type) - { - ImDrawList* drawList = gContext.mDrawList; - - if (!Intersects(op, SCALEU)) - { - return; - } - - // colors - ImU32 colors[7]; - ComputeColors(colors, type, SCALEU); - - // draw - vec_t scaleDisplay = { 1.f, 1.f, 1.f, 1.f }; - - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) - { - scaleDisplay = gContext.mScale; - } + } + } + + // draw screen cirle + drawList->AddCircleFilled(gContext.mScreenSquareCenter, gContext.mStyle.CenterCircleSize, colors[0], 32); + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsScaleType(type)) + { + //ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection); + ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + /*vec_t dif(destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y); + dif.Normalize(); + dif *= 5.f; + drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor); + drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor); + drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, destinationPosOnScreen.y - dif.y), translationLineColor, 2.f); + */ + char tmps[512]; + //vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin; + int componentInfoIndex = (type - MT_SCALE_X) * 3; + ImFormatString(tmps, sizeof(tmps), scaleInfoMask[type - MT_SCALE_X], scaleDisplay[translationInfoIndex[componentInfoIndex]]); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); + } + } + + + static void DrawScaleUniveralGizmo(OPERATION op, int type) + { + ImDrawList* drawList = gContext.mDrawList; + + if (!Intersects(op, SCALEU)) + { + return; + } + + // colors + ImU32 colors[7]; + ComputeColors(colors, type, SCALEU); + + // draw + vec_t scaleDisplay = { 1.f, 1.f, 1.f, 1.f }; + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) + { + scaleDisplay = gContext.mScale; + } + + for (int i = 0; i < 3; i++) + { + if (!Intersects(op, static_cast(SCALE_XU << i))) + { + continue; + } + const bool usingAxis = (gContext.mbUsing && type == MT_SCALE_X + i); + if (!gContext.mbUsing || usingAxis) + { + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); - for (int i = 0; i < 3; i++) - { - if (!Intersects(op, static_cast(SCALE_XU << i))) - { - continue; - } - const bool usingAxis = (gContext.mbUsing && type == MT_SCALE_X + i); - if (!gContext.mbUsing || usingAxis) + // draw axis + if (belowAxisLimit) { - vec_t dirPlaneX, dirPlaneY, dirAxis; - bool belowAxisLimit, belowPlaneLimit; - ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); - - // draw axis - if (belowAxisLimit) - { - bool hasTranslateOnAxis = Contains(op, static_cast(TRANSLATE_X << i)); - float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f; - // ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVPLocal); - // ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP); - ImVec2 worldDirSSpace = - worldToPos((dirAxis * markerScale * scaleDisplay[i]) * gContext.mScreenFactor, gContext.mMVPLocal); + bool hasTranslateOnAxis = Contains(op, static_cast(TRANSLATE_X << i)); + float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f; + //ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVPLocal); + //ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace = worldToPos((dirAxis * markerScale * scaleDisplay[i]) * gContext.mScreenFactor, gContext.mMVPLocal); #if 0 if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID)) @@ -1710,1680 +1524,1622 @@ namespace IMGUIZMO_NAMESPACE } */ #endif - drawList->AddCircleFilled(worldDirSSpace, 12.f, colors[i + 1]); - } - } - } - - // draw screen cirle - drawList->AddCircle(gContext.mScreenSquareCenter, 20.f, colors[0], 32, gContext.mStyle.CenterCircleSize); - - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsScaleType(type)) - { - // ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection); - ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); - /*vec_t dif(destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y); - dif.Normalize(); - dif *= 5.f; - drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor); - drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor); - drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, - destinationPosOnScreen.y - dif.y), translationLineColor, 2.f); - */ - char tmps[512]; - // vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin; - int componentInfoIndex = (type - MT_SCALE_X) * 3; - ImFormatString(tmps, sizeof(tmps), scaleInfoMask[type - MT_SCALE_X], scaleDisplay[translationInfoIndex[componentInfoIndex]]); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); - } - } - - static void DrawTranslationGizmo(OPERATION op, int type) - { - ImDrawList* drawList = gContext.mDrawList; - if (!drawList) - { - return; - } - - if (!Intersects(op, TRANSLATE)) - { - return; - } - - // colors - ImU32 colors[7]; - ComputeColors(colors, type, TRANSLATE); - - const ImVec2 origin = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); - - // draw - bool belowAxisLimit = false; - bool belowPlaneLimit = false; - for (int i = 0; i < 3; ++i) - { - vec_t dirPlaneX, dirPlaneY, dirAxis; - ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit); - - if (!gContext.mbUsing || (gContext.mbUsing && type == MT_MOVE_X + i)) - { - // draw axis - if (belowAxisLimit && Intersects(op, static_cast(TRANSLATE_X << i))) - { - ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVP); - ImVec2 worldDirSSpace = worldToPos(dirAxis * gContext.mScreenFactor, gContext.mMVP); - - drawList->AddLine(baseSSpace, worldDirSSpace, colors[i + 1], gContext.mStyle.TranslationLineThickness); - - // Arrow head begin - ImVec2 dir(origin - worldDirSSpace); - - float d = sqrtf(ImLengthSqr(dir)); - dir /= d; // Normalize - dir *= gContext.mStyle.TranslationLineArrowSize; - - ImVec2 ortogonalDir(dir.y, -dir.x); // Perpendicular vector - ImVec2 a(worldDirSSpace + dir); - drawList->AddTriangleFilled(worldDirSSpace - dir, a + ortogonalDir, a - ortogonalDir, colors[i + 1]); - // Arrow head end - - if (gContext.mAxisFactor[i] < 0.f) - { - DrawHatchedAxis(dirAxis); - } - } - } - // draw plane - if (!gContext.mbUsing || (gContext.mbUsing && type == MT_MOVE_YZ + i)) + drawList->AddCircleFilled(worldDirSSpace, 12.f, colors[i + 1]); + } + } + } + + // draw screen cirle + drawList->AddCircle(gContext.mScreenSquareCenter, 20.f, colors[0], 32, gContext.mStyle.CenterCircleSize); + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsScaleType(type)) + { + //ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection); + ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + /*vec_t dif(destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y); + dif.Normalize(); + dif *= 5.f; + drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor); + drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor); + drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, destinationPosOnScreen.y - dif.y), translationLineColor, 2.f); + */ + char tmps[512]; + //vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin; + int componentInfoIndex = (type - MT_SCALE_X) * 3; + ImFormatString(tmps, sizeof(tmps), scaleInfoMask[type - MT_SCALE_X], scaleDisplay[translationInfoIndex[componentInfoIndex]]); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); + } + } + + static void DrawTranslationGizmo(OPERATION op, int type) + { + ImDrawList* drawList = gContext.mDrawList; + if (!drawList) + { + return; + } + + if(!Intersects(op, TRANSLATE)) + { + return; + } + + // colors + ImU32 colors[7]; + ComputeColors(colors, type, TRANSLATE); + + const ImVec2 origin = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + + // draw + bool belowAxisLimit = false; + bool belowPlaneLimit = false; + for (int i = 0; i < 3; ++i) + { + vec_t dirPlaneX, dirPlaneY, dirAxis; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit); + + if (!gContext.mbUsing || (gContext.mbUsing && type == MT_MOVE_X + i)) + { + // draw axis + if (belowAxisLimit && Intersects(op, static_cast(TRANSLATE_X << i))) { - if (belowPlaneLimit && Contains(op, TRANSLATE_PLANS[i])) - { - ImVec2 screenQuadPts[4]; - for (int j = 0; j < 4; ++j) - { - vec_t cornerWorldPos = (dirPlaneX * quadUV[j * 2] + dirPlaneY * quadUV[j * 2 + 1]) * gContext.mScreenFactor; - screenQuadPts[j] = worldToPos(cornerWorldPos, gContext.mMVP); - } - drawList->AddPolyline(screenQuadPts, 4, GetColorU32(DIRECTION_X + i), true, 1.0f); - drawList->AddConvexPolyFilled(screenQuadPts, 4, colors[i + 4]); - } - } - } + ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace = worldToPos(dirAxis * gContext.mScreenFactor, gContext.mMVP); - drawList->AddCircleFilled(gContext.mScreenSquareCenter, gContext.mStyle.CenterCircleSize, colors[0], 32); + drawList->AddLine(baseSSpace, worldDirSSpace, colors[i + 1], gContext.mStyle.TranslationLineThickness); - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsTranslateType(type)) - { - ImU32 translationLineColor = GetColorU32(TRANSLATION_LINE); + // Arrow head begin + ImVec2 dir(origin - worldDirSSpace); - ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection); - ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); - vec_t dif = { destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y, 0.f, 0.f }; - dif.Normalize(); - dif *= 5.f; - drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor); - drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor); - drawList->AddLine( - ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), - ImVec2(destinationPosOnScreen.x - dif.x, destinationPosOnScreen.y - dif.y), - translationLineColor, - 2.f); - - char tmps[512]; - vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin; - int componentInfoIndex = (type - MT_MOVE_X) * 3; - ImFormatString( - tmps, - sizeof(tmps), - translationInfoMask[type - MT_MOVE_X], - deltaInfo[translationInfoIndex[componentInfoIndex]], - deltaInfo[translationInfoIndex[componentInfoIndex + 1]], - deltaInfo[translationInfoIndex[componentInfoIndex + 2]]); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); - } - } - - static bool CanActivate() - { - if (ImGui::IsMouseClicked(0) && !ImGui::IsAnyItemHovered() && !ImGui::IsAnyItemActive()) - { - return true; - } - return false; - } - - static void HandleAndDrawLocalBounds(const float* bounds, matrix_t* matrix, const float* snapValues, OPERATION operation) - { - ImGuiIO& io = ImGui::GetIO(); - ImDrawList* drawList = gContext.mDrawList; - - // compute best projection axis - vec_t axesWorldDirections[3]; - vec_t bestAxisWorldDirection = { 0.0f, 0.0f, 0.0f, 0.0f }; - int axes[3]; - unsigned int numAxes = 1; - axes[0] = gContext.mBoundsBestAxis; - int bestAxis = axes[0]; - if (!gContext.mbUsingBounds) - { - numAxes = 0; - float bestDot = 0.f; - for (int i = 0; i < 3; i++) - { - vec_t dirPlaneNormalWorld; - dirPlaneNormalWorld.TransformVector(directionUnary[i], gContext.mModelSource); - dirPlaneNormalWorld.Normalize(); - - float dt = fabsf(Dot(Normalized(gContext.mCameraEye - gContext.mModelSource.v.position), dirPlaneNormalWorld)); - if (dt >= bestDot) - { - bestDot = dt; - bestAxis = i; - bestAxisWorldDirection = dirPlaneNormalWorld; - } - - if (dt >= 0.1f) - { - axes[numAxes] = i; - axesWorldDirections[numAxes] = dirPlaneNormalWorld; - ++numAxes; - } - } - } - - if (numAxes == 0) - { - axes[0] = bestAxis; - axesWorldDirections[0] = bestAxisWorldDirection; - numAxes = 1; - } - - else if (bestAxis != axes[0]) - { - unsigned int bestIndex = 0; - for (unsigned int i = 0; i < numAxes; i++) - { - if (axes[i] == bestAxis) - { - bestIndex = i; - break; - } - } - int tempAxis = axes[0]; - axes[0] = axes[bestIndex]; - axes[bestIndex] = tempAxis; - vec_t tempDirection = axesWorldDirections[0]; - axesWorldDirections[0] = axesWorldDirections[bestIndex]; - axesWorldDirections[bestIndex] = tempDirection; - } - - for (unsigned int axisIndex = 0; axisIndex < numAxes; ++axisIndex) - { - bestAxis = axes[axisIndex]; - bestAxisWorldDirection = axesWorldDirections[axisIndex]; - - // corners - vec_t aabb[4]; - - int secondAxis = (bestAxis + 1) % 3; - int thirdAxis = (bestAxis + 2) % 3; - - for (int i = 0; i < 4; i++) - { - aabb[i][3] = aabb[i][bestAxis] = 0.f; - aabb[i][secondAxis] = bounds[secondAxis + 3 * (i >> 1)]; - aabb[i][thirdAxis] = bounds[thirdAxis + 3 * ((i >> 1) ^ (i & 1))]; - } + float d = sqrtf(ImLengthSqr(dir)); + dir /= d; // Normalize + dir *= gContext.mStyle.TranslationLineArrowSize; - // draw bounds - unsigned int anchorAlpha = gContext.mbEnable ? IM_COL32_BLACK : IM_COL32(0, 0, 0, 0x80); + ImVec2 ortogonalDir(dir.y, -dir.x); // Perpendicular vector + ImVec2 a(worldDirSSpace + dir); + drawList->AddTriangleFilled(worldDirSSpace - dir, a + ortogonalDir, a - ortogonalDir, colors[i + 1]); + // Arrow head end - matrix_t boundsMVP = gContext.mModelSource * gContext.mViewProjection; - for (int i = 0; i < 4; i++) - { - ImVec2 worldBound1 = worldToPos(aabb[i], boundsMVP); - ImVec2 worldBound2 = worldToPos(aabb[(i + 1) % 4], boundsMVP); - if (!IsInContextRect(worldBound1) || !IsInContextRect(worldBound2)) - { - continue; - } - float boundDistance = sqrtf(ImLengthSqr(worldBound1 - worldBound2)); - int stepCount = (int)(boundDistance / 10.f); - stepCount = min(stepCount, 1000); - for (int j = 0; j < stepCount; j++) - { - float stepLength = 1.f / (float)stepCount; - float t1 = (float)j * stepLength; - float t2 = (float)j * stepLength + stepLength * 0.5f; - ImVec2 worldBoundSS1 = ImLerp(worldBound1, worldBound2, ImVec2(t1, t1)); - ImVec2 worldBoundSS2 = ImLerp(worldBound1, worldBound2, ImVec2(t2, t2)); - // drawList->AddLine(worldBoundSS1, worldBoundSS2, IM_COL32(0, 0, 0, 0) + anchorAlpha, 3.f); - drawList->AddLine(worldBoundSS1, worldBoundSS2, IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha, 2.f); - } - vec_t midPoint = (aabb[i] + aabb[(i + 1) % 4]) * 0.5f; - ImVec2 midBound = worldToPos(midPoint, boundsMVP); - static const float AnchorBigRadius = 8.f; - static const float AnchorSmallRadius = 6.f; - bool overBigAnchor = ImLengthSqr(worldBound1 - io.MousePos) <= (AnchorBigRadius * AnchorBigRadius); - bool overSmallAnchor = ImLengthSqr(midBound - io.MousePos) <= (AnchorBigRadius * AnchorBigRadius); - - int type = MT_NONE; - vec_t gizmoHitProportion; - - if (Intersects(operation, TRANSLATE)) - { - type = GetMoveType(operation, &gizmoHitProportion); - } - if (Intersects(operation, ROTATE) && type == MT_NONE) - { - type = GetRotateType(operation); - } - if (Intersects(operation, SCALE) && type == MT_NONE) - { - type = GetScaleType(operation); - } - - if (type != MT_NONE) - { - overBigAnchor = false; - overSmallAnchor = false; - } - - ImU32 selectionColor = GetColorU32(SELECTION); - - unsigned int bigAnchorColor = overBigAnchor ? selectionColor : (IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha); - unsigned int smallAnchorColor = overSmallAnchor ? selectionColor : (IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha); - - drawList->AddCircleFilled(worldBound1, AnchorBigRadius, IM_COL32_BLACK); - drawList->AddCircleFilled(worldBound1, AnchorBigRadius - 1.2f, bigAnchorColor); - - drawList->AddCircleFilled(midBound, AnchorSmallRadius, IM_COL32_BLACK); - drawList->AddCircleFilled(midBound, AnchorSmallRadius - 1.2f, smallAnchorColor); - int oppositeIndex = (i + 2) % 4; - // big anchor on corners - if (!gContext.mbUsingBounds && gContext.mbEnable && overBigAnchor && CanActivate()) - { - gContext.mBoundsPivot.TransformPoint(aabb[(i + 2) % 4], gContext.mModelSource); - gContext.mBoundsAnchor.TransformPoint(aabb[i], gContext.mModelSource); - gContext.mBoundsPlan = BuildPlan(gContext.mBoundsAnchor, bestAxisWorldDirection); - gContext.mBoundsBestAxis = bestAxis; - gContext.mBoundsAxis[0] = secondAxis; - gContext.mBoundsAxis[1] = thirdAxis; - - gContext.mBoundsLocalPivot.Set(0.f); - gContext.mBoundsLocalPivot[secondAxis] = aabb[oppositeIndex][secondAxis]; - gContext.mBoundsLocalPivot[thirdAxis] = aabb[oppositeIndex][thirdAxis]; - - gContext.mbUsingBounds = true; - gContext.mEditingID = gContext.GetCurrentID(); - gContext.mBoundsMatrix = gContext.mModelSource; - } - // small anchor on middle of segment - if (!gContext.mbUsingBounds && gContext.mbEnable && overSmallAnchor && CanActivate()) - { - vec_t midPointOpposite = (aabb[(i + 2) % 4] + aabb[(i + 3) % 4]) * 0.5f; - gContext.mBoundsPivot.TransformPoint(midPointOpposite, gContext.mModelSource); - gContext.mBoundsAnchor.TransformPoint(midPoint, gContext.mModelSource); - gContext.mBoundsPlan = BuildPlan(gContext.mBoundsAnchor, bestAxisWorldDirection); - gContext.mBoundsBestAxis = bestAxis; - int indices[] = { secondAxis, thirdAxis }; - gContext.mBoundsAxis[0] = indices[i % 2]; - gContext.mBoundsAxis[1] = -1; - - gContext.mBoundsLocalPivot.Set(0.f); - gContext.mBoundsLocalPivot[gContext.mBoundsAxis[0]] = - aabb[oppositeIndex][indices[i % 2]]; // bounds[gContext.mBoundsAxis[0]] * (((i + 1) & 2) ? 1.f : -1.f); - - gContext.mbUsingBounds = true; - gContext.mEditingID = gContext.GetCurrentID(); - gContext.mBoundsMatrix = gContext.mModelSource; - } + if (gContext.mAxisFactor[i] < 0.f) + { + DrawHatchedAxis(dirAxis); + } } - - if (gContext.mbUsingBounds && (gContext.GetCurrentID() == gContext.mEditingID)) + } + // draw plane + if (!gContext.mbUsing || (gContext.mbUsing && type == MT_MOVE_YZ + i)) + { + if (belowPlaneLimit && Contains(op, TRANSLATE_PLANS[i])) { - matrix_t scale; - scale.SetToIdentity(); - - // compute projected mouse position on plan - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mBoundsPlan); - vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; - - // compute a reference and delta vectors base on mouse move - vec_t deltaVector = (newPos - gContext.mBoundsPivot).Abs(); - vec_t referenceVector = (gContext.mBoundsAnchor - gContext.mBoundsPivot).Abs(); - - // for 1 or 2 axes, compute a ratio that's used for scale and snap it based on resulting length - for (int i = 0; i < 2; i++) - { - int axisIndex1 = gContext.mBoundsAxis[i]; - if (axisIndex1 == -1) - { - continue; - } - - float ratioAxis = 1.f; - vec_t axisDir = gContext.mBoundsMatrix.component[axisIndex1].Abs(); - - float dtAxis = axisDir.Dot(referenceVector); - float boundSize = bounds[axisIndex1 + 3] - bounds[axisIndex1]; - if (dtAxis > FLT_EPSILON) - { - ratioAxis = axisDir.Dot(deltaVector) / dtAxis; - } - - if (snapValues) - { - float length = boundSize * ratioAxis; - ComputeSnap(&length, snapValues[axisIndex1]); - if (boundSize > FLT_EPSILON) - { - ratioAxis = length / boundSize; - } - } - scale.component[axisIndex1] *= ratioAxis; - } - - // transform matrix - matrix_t preScale, postScale; - preScale.Translation(-gContext.mBoundsLocalPivot); - postScale.Translation(gContext.mBoundsLocalPivot); - matrix_t res = preScale * scale * postScale * gContext.mBoundsMatrix; - *matrix = res; - - // info text - char tmps[512]; - ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); - ImFormatString( - tmps, - sizeof(tmps), - "X: %.2f Y: %.2f Z: %.2f", - (bounds[3] - bounds[0]) * gContext.mBoundsMatrix.component[0].Length() * scale.component[0].Length(), - (bounds[4] - bounds[1]) * gContext.mBoundsMatrix.component[1].Length() * scale.component[1].Length(), - (bounds[5] - bounds[2]) * gContext.mBoundsMatrix.component[2].Length() * scale.component[2].Length()); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); - drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); - } + ImVec2 screenQuadPts[4]; + for (int j = 0; j < 4; ++j) + { + vec_t cornerWorldPos = (dirPlaneX * quadUV[j * 2] + dirPlaneY * quadUV[j * 2 + 1]) * gContext.mScreenFactor; + screenQuadPts[j] = worldToPos(cornerWorldPos, gContext.mMVP); + } + drawList->AddPolyline(screenQuadPts, 4, GetColorU32(DIRECTION_X + i), true, 1.0f); + drawList->AddConvexPolyFilled(screenQuadPts, 4, colors[i + 4]); + } + } + } + + drawList->AddCircleFilled(gContext.mScreenSquareCenter, gContext.mStyle.CenterCircleSize, colors[0], 32); + + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsTranslateType(type)) + { + ImU32 translationLineColor = GetColorU32(TRANSLATION_LINE); + + ImVec2 sourcePosOnScreen = worldToPos(gContext.mMatrixOrigin, gContext.mViewProjection); + ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + vec_t dif = { destinationPosOnScreen.x - sourcePosOnScreen.x, destinationPosOnScreen.y - sourcePosOnScreen.y, 0.f, 0.f }; + dif.Normalize(); + dif *= 5.f; + drawList->AddCircle(sourcePosOnScreen, 6.f, translationLineColor); + drawList->AddCircle(destinationPosOnScreen, 6.f, translationLineColor); + drawList->AddLine(ImVec2(sourcePosOnScreen.x + dif.x, sourcePosOnScreen.y + dif.y), ImVec2(destinationPosOnScreen.x - dif.x, destinationPosOnScreen.y - dif.y), translationLineColor, 2.f); + + char tmps[512]; + vec_t deltaInfo = gContext.mModel.v.position - gContext.mMatrixOrigin; + int componentInfoIndex = (type - MT_MOVE_X) * 3; + ImFormatString(tmps, sizeof(tmps), translationInfoMask[type - MT_MOVE_X], deltaInfo[translationInfoIndex[componentInfoIndex]], deltaInfo[translationInfoIndex[componentInfoIndex + 1]], deltaInfo[translationInfoIndex[componentInfoIndex + 2]]); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); + } + } + + static bool CanActivate() + { + if (ImGui::IsMouseClicked(0) && !ImGui::IsAnyItemHovered() && !ImGui::IsAnyItemActive()) + { + return true; + } + return false; + } + + static void HandleAndDrawLocalBounds(const float* bounds, matrix_t* matrix, const float* snapValues, OPERATION operation) + { + ImGuiIO& io = ImGui::GetIO(); + ImDrawList* drawList = gContext.mDrawList; + + // compute best projection axis + vec_t axesWorldDirections[3]; + vec_t bestAxisWorldDirection = { 0.0f, 0.0f, 0.0f, 0.0f }; + int axes[3]; + unsigned int numAxes = 1; + axes[0] = gContext.mBoundsBestAxis; + int bestAxis = axes[0]; + if (!gContext.mbUsingBounds) + { + numAxes = 0; + float bestDot = 0.f; + for (int i = 0; i < 3; i++) + { + vec_t dirPlaneNormalWorld; + dirPlaneNormalWorld.TransformVector(directionUnary[i], gContext.mModelSource); + dirPlaneNormalWorld.Normalize(); + + float dt = fabsf(Dot(Normalized(gContext.mCameraEye - gContext.mModelSource.v.position), dirPlaneNormalWorld)); + if (dt >= bestDot) + { + bestDot = dt; + bestAxis = i; + bestAxisWorldDirection = dirPlaneNormalWorld; + } + + if (dt >= 0.1f) + { + axes[numAxes] = i; + axesWorldDirections[numAxes] = dirPlaneNormalWorld; + ++numAxes; + } + } + } + + if (numAxes == 0) + { + axes[0] = bestAxis; + axesWorldDirections[0] = bestAxisWorldDirection; + numAxes = 1; + } + + else if (bestAxis != axes[0]) + { + unsigned int bestIndex = 0; + for (unsigned int i = 0; i < numAxes; i++) + { + if (axes[i] == bestAxis) + { + bestIndex = i; + break; + } + } + int tempAxis = axes[0]; + axes[0] = axes[bestIndex]; + axes[bestIndex] = tempAxis; + vec_t tempDirection = axesWorldDirections[0]; + axesWorldDirections[0] = axesWorldDirections[bestIndex]; + axesWorldDirections[bestIndex] = tempDirection; + } + + for (unsigned int axisIndex = 0; axisIndex < numAxes; ++axisIndex) + { + bestAxis = axes[axisIndex]; + bestAxisWorldDirection = axesWorldDirections[axisIndex]; + + // corners + vec_t aabb[4]; + + int secondAxis = (bestAxis + 1) % 3; + int thirdAxis = (bestAxis + 2) % 3; + + for (int i = 0; i < 4; i++) + { + aabb[i][3] = aabb[i][bestAxis] = 0.f; + aabb[i][secondAxis] = bounds[secondAxis + 3 * (i >> 1)]; + aabb[i][thirdAxis] = bounds[thirdAxis + 3 * ((i >> 1) ^ (i & 1))]; + } + + // draw bounds + unsigned int anchorAlpha = gContext.mbEnable ? IM_COL32_BLACK : IM_COL32(0, 0, 0, 0x80); + + matrix_t boundsMVP = gContext.mModelSource * gContext.mViewProjection; + for (int i = 0; i < 4; i++) + { + ImVec2 worldBound1 = worldToPos(aabb[i], boundsMVP); + ImVec2 worldBound2 = worldToPos(aabb[(i + 1) % 4], boundsMVP); + if (!IsInContextRect(worldBound1) || !IsInContextRect(worldBound2)) + { + continue; + } + float boundDistance = sqrtf(ImLengthSqr(worldBound1 - worldBound2)); + int stepCount = (int)(boundDistance / 10.f); + stepCount = min(stepCount, 1000); + for (int j = 0; j < stepCount; j++) + { + float stepLength = 1.f / (float)stepCount; + float t1 = (float)j * stepLength; + float t2 = (float)j * stepLength + stepLength * 0.5f; + ImVec2 worldBoundSS1 = ImLerp(worldBound1, worldBound2, ImVec2(t1, t1)); + ImVec2 worldBoundSS2 = ImLerp(worldBound1, worldBound2, ImVec2(t2, t2)); + //drawList->AddLine(worldBoundSS1, worldBoundSS2, IM_COL32(0, 0, 0, 0) + anchorAlpha, 3.f); + drawList->AddLine(worldBoundSS1, worldBoundSS2, IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha, 2.f); + } + vec_t midPoint = (aabb[i] + aabb[(i + 1) % 4]) * 0.5f; + ImVec2 midBound = worldToPos(midPoint, boundsMVP); + static const float AnchorBigRadius = 8.f; + static const float AnchorSmallRadius = 6.f; + bool overBigAnchor = ImLengthSqr(worldBound1 - io.MousePos) <= (AnchorBigRadius * AnchorBigRadius); + bool overSmallAnchor = ImLengthSqr(midBound - io.MousePos) <= (AnchorBigRadius * AnchorBigRadius); + + int type = MT_NONE; + vec_t gizmoHitProportion; - if (!io.MouseDown[0]) + if(Intersects(operation, TRANSLATE)) { - gContext.mbUsingBounds = false; - gContext.mEditingID = -1; + type = GetMoveType(operation, &gizmoHitProportion); } - if (gContext.mbUsingBounds) + if(Intersects(operation, ROTATE) && type == MT_NONE) { - break; + type = GetRotateType(operation); } - } - } - - /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // - - static int GetScaleType(OPERATION op) - { - if (gContext.mbUsing) - { - return MT_NONE; - } - ImGuiIO& io = ImGui::GetIO(); - int type = MT_NONE; - - // screen - if (io.MousePos.x >= gContext.mScreenSquareMin.x && io.MousePos.x <= gContext.mScreenSquareMax.x && - io.MousePos.y >= gContext.mScreenSquareMin.y && io.MousePos.y <= gContext.mScreenSquareMax.y && Contains(op, SCALE)) - { - type = MT_SCALE_XYZ; - } - - // compute - for (int i = 0; i < 3 && type == MT_NONE; i++) - { - if (!Intersects(op, static_cast(SCALE_X << i))) + if(Intersects(operation, SCALE) && type == MT_NONE) { - continue; + type = GetScaleType(operation); } - bool isAxisMasked = (1 << i) & gContext.mAxisMask; - vec_t dirPlaneX, dirPlaneY, dirAxis; - bool belowAxisLimit, belowPlaneLimit; - ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); - dirAxis.TransformVector(gContext.mModelLocal); - dirPlaneX.TransformVector(gContext.mModelLocal); - dirPlaneY.TransformVector(gContext.mModelLocal); - - const float len = - IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, BuildPlan(gContext.mModelLocal.v.position, dirAxis)); - vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len; - - const float startOffset = Contains(op, static_cast(TRANSLATE_X << i)) ? 1.0f : 0.1f; - const float endOffset = Contains(op, static_cast(TRANSLATE_X << i)) ? 1.4f : 1.0f; - const ImVec2 posOnPlanScreen = worldToPos(posOnPlan, gContext.mViewProjection); - const ImVec2 axisStartOnScreen = - worldToPos(gContext.mModelLocal.v.position + dirAxis * gContext.mScreenFactor * startOffset, gContext.mViewProjection); - const ImVec2 axisEndOnScreen = - worldToPos(gContext.mModelLocal.v.position + dirAxis * gContext.mScreenFactor * endOffset, gContext.mViewProjection); - - vec_t closestPointOnAxis = PointOnSegment(makeVect(posOnPlanScreen), makeVect(axisStartOnScreen), makeVect(axisEndOnScreen)); - - if ((closestPointOnAxis - makeVect(posOnPlanScreen)).Length() < 12.f) // pixel size + if (type != MT_NONE) { - if (!isAxisMasked) - type = MT_SCALE_X + i; + overBigAnchor = false; + overSmallAnchor = false; } - } - // universal - - vec_t deltaScreen = { io.MousePos.x - gContext.mScreenSquareCenter.x, io.MousePos.y - gContext.mScreenSquareCenter.y, 0.f, 0.f }; - float dist = deltaScreen.Length(); - if (Contains(op, SCALEU) && dist >= 17.0f && dist < 23.0f) - { - type = MT_SCALE_XYZ; - } + ImU32 selectionColor = GetColorU32(SELECTION); - for (int i = 0; i < 3 && type == MT_NONE; i++) - { - if (!Intersects(op, static_cast(SCALE_XU << i))) - { - continue; - } + unsigned int bigAnchorColor = overBigAnchor ? selectionColor : (IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha); + unsigned int smallAnchorColor = overSmallAnchor ? selectionColor : (IM_COL32(0xAA, 0xAA, 0xAA, 0) + anchorAlpha); + + drawList->AddCircleFilled(worldBound1, AnchorBigRadius, IM_COL32_BLACK); + drawList->AddCircleFilled(worldBound1, AnchorBigRadius - 1.2f, bigAnchorColor); + + drawList->AddCircleFilled(midBound, AnchorSmallRadius, IM_COL32_BLACK); + drawList->AddCircleFilled(midBound, AnchorSmallRadius - 1.2f, smallAnchorColor); + int oppositeIndex = (i + 2) % 4; + // big anchor on corners + if (!gContext.mbUsingBounds && gContext.mbEnable && overBigAnchor && CanActivate()) + { + gContext.mBoundsPivot.TransformPoint(aabb[(i + 2) % 4], gContext.mModelSource); + gContext.mBoundsAnchor.TransformPoint(aabb[i], gContext.mModelSource); + gContext.mBoundsPlan = BuildPlan(gContext.mBoundsAnchor, bestAxisWorldDirection); + gContext.mBoundsBestAxis = bestAxis; + gContext.mBoundsAxis[0] = secondAxis; + gContext.mBoundsAxis[1] = thirdAxis; + + gContext.mBoundsLocalPivot.Set(0.f); + gContext.mBoundsLocalPivot[secondAxis] = aabb[oppositeIndex][secondAxis]; + gContext.mBoundsLocalPivot[thirdAxis] = aabb[oppositeIndex][thirdAxis]; + + gContext.mbUsingBounds = true; + gContext.mEditingID = gContext.GetCurrentID(); + gContext.mBoundsMatrix = gContext.mModelSource; + } + // small anchor on middle of segment + if (!gContext.mbUsingBounds && gContext.mbEnable && overSmallAnchor && CanActivate()) + { + vec_t midPointOpposite = (aabb[(i + 2) % 4] + aabb[(i + 3) % 4]) * 0.5f; + gContext.mBoundsPivot.TransformPoint(midPointOpposite, gContext.mModelSource); + gContext.mBoundsAnchor.TransformPoint(midPoint, gContext.mModelSource); + gContext.mBoundsPlan = BuildPlan(gContext.mBoundsAnchor, bestAxisWorldDirection); + gContext.mBoundsBestAxis = bestAxis; + int indices[] = { secondAxis , thirdAxis }; + gContext.mBoundsAxis[0] = indices[i % 2]; + gContext.mBoundsAxis[1] = -1; + + gContext.mBoundsLocalPivot.Set(0.f); + gContext.mBoundsLocalPivot[gContext.mBoundsAxis[0]] = aabb[oppositeIndex][indices[i % 2]];// bounds[gContext.mBoundsAxis[0]] * (((i + 1) & 2) ? 1.f : -1.f); + + gContext.mbUsingBounds = true; + gContext.mEditingID = gContext.GetCurrentID(); + gContext.mBoundsMatrix = gContext.mModelSource; + } + } + + if (gContext.mbUsingBounds && (gContext.GetCurrentID() == gContext.mEditingID)) + { + matrix_t scale; + scale.SetToIdentity(); + + // compute projected mouse position on plan + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mBoundsPlan); + vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; - vec_t dirPlaneX, dirPlaneY, dirAxis; - bool belowAxisLimit, belowPlaneLimit; - ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); + // compute a reference and delta vectors base on mouse move + vec_t deltaVector = (newPos - gContext.mBoundsPivot).Abs(); + vec_t referenceVector = (gContext.mBoundsAnchor - gContext.mBoundsPivot).Abs(); - // draw axis - if (belowAxisLimit) + // for 1 or 2 axes, compute a ratio that's used for scale and snap it based on resulting length + for (int i = 0; i < 2; i++) { - bool hasTranslateOnAxis = Contains(op, static_cast(TRANSLATE_X << i)); - float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f; - // ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVPLocal); - // ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP); - ImVec2 worldDirSSpace = worldToPos((dirAxis * markerScale) * gContext.mScreenFactor, gContext.mMVPLocal); - - float distance = sqrtf(ImLengthSqr(worldDirSSpace - io.MousePos)); - if (distance < 12.f) - { - type = MT_SCALE_X + i; - } - } - } - return type; - } - - static int GetRotateType(OPERATION op) - { - if (gContext.mbUsing) - { - return MT_NONE; - } - - bool isNoAxesMasked = !gContext.mAxisMask; - bool isMultipleAxesMasked = gContext.mAxisMask & (gContext.mAxisMask - 1); - - ImGuiIO& io = ImGui::GetIO(); - int type = MT_NONE; - - vec_t deltaScreen = { io.MousePos.x - gContext.mScreenSquareCenter.x, io.MousePos.y - gContext.mScreenSquareCenter.y, 0.f, 0.f }; - float dist = deltaScreen.Length(); - if (Intersects(op, ROTATE_SCREEN) && dist >= (gContext.mRadiusSquareCenter - 4.0f) && dist < (gContext.mRadiusSquareCenter + 4.0f)) - { - if (!isNoAxesMasked) - return MT_NONE; - type = MT_ROTATE_SCREEN; - } - - const vec_t planNormals[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir }; - - vec_t modelViewPos; - modelViewPos.TransformPoint(gContext.mModel.v.position, gContext.mViewMat); + int axisIndex1 = gContext.mBoundsAxis[i]; + if (axisIndex1 == -1) + { + continue; + } - for (int i = 0; i < 3 && type == MT_NONE; i++) - { - if (!Intersects(op, static_cast(ROTATE_X << i))) - { - continue; - } - bool isAxisMasked = (1 << i) & gContext.mAxisMask; - // pickup plan - vec_t pickupPlan = BuildPlan(gContext.mModel.v.position, planNormals[i]); + float ratioAxis = 1.f; + vec_t axisDir = gContext.mBoundsMatrix.component[axisIndex1].Abs(); - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, pickupPlan); - const vec_t intersectWorldPos = gContext.mRayOrigin + gContext.mRayVector * len; - vec_t intersectViewPos; - intersectViewPos.TransformPoint(intersectWorldPos, gContext.mViewMat); + float dtAxis = axisDir.Dot(referenceVector); + float boundSize = bounds[axisIndex1 + 3] - bounds[axisIndex1]; + if (dtAxis > FLT_EPSILON) + { + ratioAxis = axisDir.Dot(deltaVector) / dtAxis; + } - if (ImAbs(modelViewPos.z) - ImAbs(intersectViewPos.z) < -FLT_EPSILON) - { - continue; + if (snapValues) + { + float length = boundSize * ratioAxis; + ComputeSnap(&length, snapValues[axisIndex1]); + if (boundSize > FLT_EPSILON) + { + ratioAxis = length / boundSize; + } + } + scale.component[axisIndex1] *= ratioAxis; } - const vec_t localPos = intersectWorldPos - gContext.mModel.v.position; - vec_t idealPosOnCircle = Normalized(localPos); - idealPosOnCircle.TransformVector(gContext.mModelInverse); - const ImVec2 idealPosOnCircleScreen = - worldToPos(idealPosOnCircle * rotationDisplayFactor * gContext.mScreenFactor, gContext.mMVP); - - // gContext.mDrawList->AddCircle(idealPosOnCircleScreen, 5.f, IM_COL32_WHITE); - const ImVec2 distanceOnScreen = idealPosOnCircleScreen - io.MousePos; - - const float distance = makeVect(distanceOnScreen).Length(); - if (distance < 8.f) // pixel size - { - if ((!isAxisMasked || isMultipleAxesMasked) && !isNoAxesMasked) - break; - type = MT_ROTATE_X + i; - } - } + // transform matrix + matrix_t preScale, postScale; + preScale.Translation(-gContext.mBoundsLocalPivot); + postScale.Translation(gContext.mBoundsLocalPivot); + matrix_t res = preScale * scale * postScale * gContext.mBoundsMatrix; + *matrix = res; - return type; - } + // info text + char tmps[512]; + ImVec2 destinationPosOnScreen = worldToPos(gContext.mModel.v.position, gContext.mViewProjection); + ImFormatString(tmps, sizeof(tmps), "X: %.2f Y: %.2f Z: %.2f" + , (bounds[3] - bounds[0]) * gContext.mBoundsMatrix.component[0].Length() * scale.component[0].Length() + , (bounds[4] - bounds[1]) * gContext.mBoundsMatrix.component[1].Length() * scale.component[1].Length() + , (bounds[5] - bounds[2]) * gContext.mBoundsMatrix.component[2].Length() * scale.component[2].Length() + ); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 15, destinationPosOnScreen.y + 15), GetColorU32(TEXT_SHADOW), tmps); + drawList->AddText(ImVec2(destinationPosOnScreen.x + 14, destinationPosOnScreen.y + 14), GetColorU32(TEXT), tmps); + } - static int GetMoveType(OPERATION op, vec_t* gizmoHitProportion) - { - if (!Intersects(op, TRANSLATE) || gContext.mbUsing || !gContext.mbMouseOver) - { + if (!io.MouseDown[0]) { + gContext.mbUsingBounds = false; + gContext.mEditingID = -1; + } + if (gContext.mbUsingBounds) + { + break; + } + } + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // + + static int GetScaleType(OPERATION op) + { + if (gContext.mbUsing) + { + return MT_NONE; + } + ImGuiIO& io = ImGui::GetIO(); + int type = MT_NONE; + + // screen + if (io.MousePos.x >= gContext.mScreenSquareMin.x && io.MousePos.x <= gContext.mScreenSquareMax.x && + io.MousePos.y >= gContext.mScreenSquareMin.y && io.MousePos.y <= gContext.mScreenSquareMax.y && + Contains(op, SCALE)) + { + type = MT_SCALE_XYZ; + } + + // compute + for (int i = 0; i < 3 && type == MT_NONE; i++) + { + if(!Intersects(op, static_cast(SCALE_X << i))) + { + continue; + } + bool isAxisMasked = (1 << i) & gContext.mAxisMask; + + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); + dirAxis.TransformVector(gContext.mModelLocal); + dirPlaneX.TransformVector(gContext.mModelLocal); + dirPlaneY.TransformVector(gContext.mModelLocal); + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, BuildPlan(gContext.mModelLocal.v.position, dirAxis)); + vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len; + + const float startOffset = Contains(op, static_cast(TRANSLATE_X << i)) ? 1.0f : 0.1f; + const float endOffset = Contains(op, static_cast(TRANSLATE_X << i)) ? 1.4f : 1.0f; + const ImVec2 posOnPlanScreen = worldToPos(posOnPlan, gContext.mViewProjection); + const ImVec2 axisStartOnScreen = worldToPos(gContext.mModelLocal.v.position + dirAxis * gContext.mScreenFactor * startOffset, gContext.mViewProjection); + const ImVec2 axisEndOnScreen = worldToPos(gContext.mModelLocal.v.position + dirAxis * gContext.mScreenFactor * endOffset, gContext.mViewProjection); + + vec_t closestPointOnAxis = PointOnSegment(makeVect(posOnPlanScreen), makeVect(axisStartOnScreen), makeVect(axisEndOnScreen)); + + if ((closestPointOnAxis - makeVect(posOnPlanScreen)).Length() < 12.f) // pixel size + { + if (!isAxisMasked) + type = MT_SCALE_X + i; + } + } + + // universal + + vec_t deltaScreen = { io.MousePos.x - gContext.mScreenSquareCenter.x, io.MousePos.y - gContext.mScreenSquareCenter.y, 0.f, 0.f }; + float dist = deltaScreen.Length(); + if (Contains(op, SCALEU) && dist >= 17.0f && dist < 23.0f) + { + type = MT_SCALE_XYZ; + } + + for (int i = 0; i < 3 && type == MT_NONE; i++) + { + if (!Intersects(op, static_cast(SCALE_XU << i))) + { + continue; + } + + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit, true); + + // draw axis + if (belowAxisLimit) + { + bool hasTranslateOnAxis = Contains(op, static_cast(TRANSLATE_X << i)); + float markerScale = hasTranslateOnAxis ? 1.4f : 1.0f; + //ImVec2 baseSSpace = worldToPos(dirAxis * 0.1f * gContext.mScreenFactor, gContext.mMVPLocal); + //ImVec2 worldDirSSpaceNoScale = worldToPos(dirAxis * markerScale * gContext.mScreenFactor, gContext.mMVP); + ImVec2 worldDirSSpace = worldToPos((dirAxis * markerScale) * gContext.mScreenFactor, gContext.mMVPLocal); + + float distance = sqrtf(ImLengthSqr(worldDirSSpace - io.MousePos)); + if (distance < 12.f) + { + type = MT_SCALE_X + i; + } + } + } + return type; + } + + static int GetRotateType(OPERATION op) + { + if (gContext.mbUsing) + { + return MT_NONE; + } + + bool isNoAxesMasked = !gContext.mAxisMask; + bool isMultipleAxesMasked = gContext.mAxisMask & (gContext.mAxisMask - 1); + + ImGuiIO& io = ImGui::GetIO(); + int type = MT_NONE; + + vec_t deltaScreen = { io.MousePos.x - gContext.mScreenSquareCenter.x, io.MousePos.y - gContext.mScreenSquareCenter.y, 0.f, 0.f }; + float dist = deltaScreen.Length(); + if (Intersects(op, ROTATE_SCREEN) && dist >= (gContext.mRadiusSquareCenter - 4.0f) && dist < (gContext.mRadiusSquareCenter + 4.0f)) + { + if (!isNoAxesMasked) return MT_NONE; - } - - bool isNoAxesMasked = !gContext.mAxisMask; - bool isMultipleAxesMasked = gContext.mAxisMask & (gContext.mAxisMask - 1); - - ImGuiIO& io = ImGui::GetIO(); - int type = MT_NONE; - - // screen - if (io.MousePos.x >= gContext.mScreenSquareMin.x && io.MousePos.x <= gContext.mScreenSquareMax.x && - io.MousePos.y >= gContext.mScreenSquareMin.y && io.MousePos.y <= gContext.mScreenSquareMax.y && Contains(op, TRANSLATE)) - { - type = MT_MOVE_SCREEN; - } - - const vec_t screenCoord = makeVect(io.MousePos - ImVec2(gContext.mX, gContext.mY)); - - // compute - for (int i = 0; i < 3 && type == MT_NONE; i++) - { - bool isAxisMasked = (1 << i) & gContext.mAxisMask; - vec_t dirPlaneX, dirPlaneY, dirAxis; - bool belowAxisLimit, belowPlaneLimit; - ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit); - dirAxis.TransformVector(gContext.mModel); - dirPlaneX.TransformVector(gContext.mModel); - dirPlaneY.TransformVector(gContext.mModel); - - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, BuildPlan(gContext.mModel.v.position, dirAxis)); - vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len; - - const ImVec2 axisStartOnScreen = - worldToPos(gContext.mModel.v.position + dirAxis * gContext.mScreenFactor * 0.1f, gContext.mViewProjection) - - ImVec2(gContext.mX, gContext.mY); - const ImVec2 axisEndOnScreen = - worldToPos(gContext.mModel.v.position + dirAxis * gContext.mScreenFactor, gContext.mViewProjection) - - ImVec2(gContext.mX, gContext.mY); - - vec_t closestPointOnAxis = PointOnSegment(screenCoord, makeVect(axisStartOnScreen), makeVect(axisEndOnScreen)); - if ((closestPointOnAxis - screenCoord).Length() < 12.f && - Intersects(op, static_cast(TRANSLATE_X << i))) // pixel size - { - if (isAxisMasked) - break; - type = MT_MOVE_X + i; - } - - const float dx = dirPlaneX.Dot3((posOnPlan - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor)); - const float dy = dirPlaneY.Dot3((posOnPlan - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor)); - if (belowPlaneLimit && dx >= quadUV[0] && dx <= quadUV[4] && dy >= quadUV[1] && dy <= quadUV[3] && - Contains(op, TRANSLATE_PLANS[i])) - { - if ((!isAxisMasked || isMultipleAxesMasked) && !isNoAxesMasked) - break; - type = MT_MOVE_YZ + i; - } - - if (gizmoHitProportion) - { - *gizmoHitProportion = makeVect(dx, dy, 0.f); - } - } - return type; - } - - static bool HandleTranslation(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) - { - if (!Intersects(op, TRANSLATE) || type != MT_NONE) - { - return false; - } - const ImGuiIO& io = ImGui::GetIO(); - const bool applyRotationLocaly = gContext.mMode == LOCAL || type == MT_MOVE_SCREEN; - bool modified = false; - - // move - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsTranslateType(gContext.mCurrentOperation)) - { + type = MT_ROTATE_SCREEN; + } + + const vec_t planNormals[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir }; + + vec_t modelViewPos; + modelViewPos.TransformPoint(gContext.mModel.v.position, gContext.mViewMat); + + for (int i = 0; i < 3 && type == MT_NONE; i++) + { + if(!Intersects(op, static_cast(ROTATE_X << i))) + { + continue; + } + bool isAxisMasked = (1 << i) & gContext.mAxisMask; + // pickup plan + vec_t pickupPlan = BuildPlan(gContext.mModel.v.position, planNormals[i]); + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, pickupPlan); + const vec_t intersectWorldPos = gContext.mRayOrigin + gContext.mRayVector * len; + vec_t intersectViewPos; + intersectViewPos.TransformPoint(intersectWorldPos, gContext.mViewMat); + + if (ImAbs(modelViewPos.z) - ImAbs(intersectViewPos.z) < -FLT_EPSILON) + { + continue; + } + + const vec_t localPos = intersectWorldPos - gContext.mModel.v.position; + vec_t idealPosOnCircle = Normalized(localPos); + idealPosOnCircle.TransformVector(gContext.mModelInverse); + const ImVec2 idealPosOnCircleScreen = worldToPos(idealPosOnCircle * rotationDisplayFactor * gContext.mScreenFactor, gContext.mMVP); + + //gContext.mDrawList->AddCircle(idealPosOnCircleScreen, 5.f, IM_COL32_WHITE); + const ImVec2 distanceOnScreen = idealPosOnCircleScreen - io.MousePos; + + const float distance = makeVect(distanceOnScreen).Length(); + if (distance < 8.f) // pixel size + { + if ((!isAxisMasked || isMultipleAxesMasked) && !isNoAxesMasked) + break; + type = MT_ROTATE_X + i; + } + } + + return type; + } + + static int GetMoveType(OPERATION op, vec_t* gizmoHitProportion) + { + if(!Intersects(op, TRANSLATE) || gContext.mbUsing || !gContext.mbMouseOver) + { + return MT_NONE; + } + + bool isNoAxesMasked = !gContext.mAxisMask; + bool isMultipleAxesMasked = gContext.mAxisMask & (gContext.mAxisMask - 1); + + ImGuiIO& io = ImGui::GetIO(); + int type = MT_NONE; + + // screen + if (io.MousePos.x >= gContext.mScreenSquareMin.x && io.MousePos.x <= gContext.mScreenSquareMax.x && + io.MousePos.y >= gContext.mScreenSquareMin.y && io.MousePos.y <= gContext.mScreenSquareMax.y && + Contains(op, TRANSLATE)) + { + type = MT_MOVE_SCREEN; + } + + const vec_t screenCoord = makeVect(io.MousePos - ImVec2(gContext.mX, gContext.mY)); + + // compute + for (int i = 0; i < 3 && type == MT_NONE; i++) + { + bool isAxisMasked = (1 << i) & gContext.mAxisMask; + vec_t dirPlaneX, dirPlaneY, dirAxis; + bool belowAxisLimit, belowPlaneLimit; + ComputeTripodAxisAndVisibility(i, dirAxis, dirPlaneX, dirPlaneY, belowAxisLimit, belowPlaneLimit); + dirAxis.TransformVector(gContext.mModel); + dirPlaneX.TransformVector(gContext.mModel); + dirPlaneY.TransformVector(gContext.mModel); + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, BuildPlan(gContext.mModel.v.position, dirAxis)); + vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len; + + const ImVec2 axisStartOnScreen = worldToPos(gContext.mModel.v.position + dirAxis * gContext.mScreenFactor * 0.1f, gContext.mViewProjection) - ImVec2(gContext.mX, gContext.mY); + const ImVec2 axisEndOnScreen = worldToPos(gContext.mModel.v.position + dirAxis * gContext.mScreenFactor, gContext.mViewProjection) - ImVec2(gContext.mX, gContext.mY); + + vec_t closestPointOnAxis = PointOnSegment(screenCoord, makeVect(axisStartOnScreen), makeVect(axisEndOnScreen)); + if ((closestPointOnAxis - screenCoord).Length() < 12.f && Intersects(op, static_cast(TRANSLATE_X << i))) // pixel size + { + if (isAxisMasked) + break; + type = MT_MOVE_X + i; + } + + const float dx = dirPlaneX.Dot3((posOnPlan - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor)); + const float dy = dirPlaneY.Dot3((posOnPlan - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor)); + if (belowPlaneLimit && dx >= quadUV[0] && dx <= quadUV[4] && dy >= quadUV[1] && dy <= quadUV[3] && Contains(op, TRANSLATE_PLANS[i])) + { + if ((!isAxisMasked || isMultipleAxesMasked) && !isNoAxesMasked) + break; + type = MT_MOVE_YZ + i; + } + + if (gizmoHitProportion) + { + *gizmoHitProportion = makeVect(dx, dy, 0.f); + } + } + return type; + } + + static bool HandleTranslation(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) + { + if(!Intersects(op, TRANSLATE) || type != MT_NONE) + { + return false; + } + const ImGuiIO& io = ImGui::GetIO(); + const bool applyRotationLocaly = gContext.mMode == LOCAL || type == MT_MOVE_SCREEN; + bool modified = false; + + // move + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsTranslateType(gContext.mCurrentOperation)) + { #if IMGUI_VERSION_NUM >= 18723 - ImGui::SetNextFrameWantCaptureMouse(true); + ImGui::SetNextFrameWantCaptureMouse(true); #else - ImGui::CaptureMouseFromApp(); + ImGui::CaptureMouseFromApp(); #endif - const float signedLength = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); - const float len = fabsf(signedLength); // near plan - const vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; - - // compute delta - const vec_t newOrigin = newPos - gContext.mRelativeOrigin * gContext.mScreenFactor; - vec_t delta = newOrigin - gContext.mModel.v.position; - - // 1 axis constraint - if (gContext.mCurrentOperation >= MT_MOVE_X && gContext.mCurrentOperation <= MT_MOVE_Z) + const float signedLength = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + const float len = fabsf(signedLength); // near plan + const vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; + + // compute delta + const vec_t newOrigin = newPos - gContext.mRelativeOrigin * gContext.mScreenFactor; + vec_t delta = newOrigin - gContext.mModel.v.position; + + // 1 axis constraint + if (gContext.mCurrentOperation >= MT_MOVE_X && gContext.mCurrentOperation <= MT_MOVE_Z) + { + const int axisIndex = gContext.mCurrentOperation - MT_MOVE_X; + const vec_t& axisValue = *(vec_t*)&gContext.mModel.m[axisIndex]; + const float lengthOnAxis = Dot(axisValue, delta); + delta = axisValue * lengthOnAxis; + } + + // snap + if (snap) + { + vec_t cumulativeDelta = gContext.mModel.v.position + delta - gContext.mMatrixOrigin; + if (applyRotationLocaly) { - const int axisIndex = gContext.mCurrentOperation - MT_MOVE_X; - const vec_t& axisValue = *(vec_t*)&gContext.mModel.m[axisIndex]; - const float lengthOnAxis = Dot(axisValue, delta); - delta = axisValue * lengthOnAxis; + matrix_t modelSourceNormalized = gContext.mModelSource; + modelSourceNormalized.OrthoNormalize(); + matrix_t modelSourceNormalizedInverse; + modelSourceNormalizedInverse.Inverse(modelSourceNormalized); + cumulativeDelta.TransformVector(modelSourceNormalizedInverse); + ComputeSnap(cumulativeDelta, snap); + cumulativeDelta.TransformVector(modelSourceNormalized); } - - // snap - if (snap) + else { - vec_t cumulativeDelta = gContext.mModel.v.position + delta - gContext.mMatrixOrigin; - if (applyRotationLocaly) - { - matrix_t modelSourceNormalized = gContext.mModelSource; - modelSourceNormalized.OrthoNormalize(); - matrix_t modelSourceNormalizedInverse; - modelSourceNormalizedInverse.Inverse(modelSourceNormalized); - cumulativeDelta.TransformVector(modelSourceNormalizedInverse); - ComputeSnap(cumulativeDelta, snap); - cumulativeDelta.TransformVector(modelSourceNormalized); - } - else - { - ComputeSnap(cumulativeDelta, snap); - } - delta = gContext.mMatrixOrigin + cumulativeDelta - gContext.mModel.v.position; + ComputeSnap(cumulativeDelta, snap); } + delta = gContext.mMatrixOrigin + cumulativeDelta - gContext.mModel.v.position; - if (delta != gContext.mTranslationLastDelta) - { - modified = true; - } - gContext.mTranslationLastDelta = delta; + } - // compute matrix & delta - matrix_t deltaMatrixTranslation; - deltaMatrixTranslation.Translation(delta); - if (deltaMatrix) - { - memcpy(deltaMatrix, deltaMatrixTranslation.m16, sizeof(float) * 16); - } + if (delta != gContext.mTranslationLastDelta) + { + modified = true; + } + gContext.mTranslationLastDelta = delta; - const matrix_t res = gContext.mModelSource * deltaMatrixTranslation; - *(matrix_t*)matrix = res; + // compute matrix & delta + matrix_t deltaMatrixTranslation; + deltaMatrixTranslation.Translation(delta); + if (deltaMatrix) + { + memcpy(deltaMatrix, deltaMatrixTranslation.m16, sizeof(float) * 16); + } - if (!io.MouseDown[0]) - { - gContext.mbUsing = false; - } + const matrix_t res = gContext.mModelSource * deltaMatrixTranslation; + *(matrix_t*)matrix = res; - type = gContext.mCurrentOperation; - } - else - { - // find new possible way to move - vec_t gizmoHitProportion; - type = gContext.mbOverGizmoHotspot ? MT_NONE : GetMoveType(op, &gizmoHitProportion); - gContext.mbOverGizmoHotspot |= type != MT_NONE; - if (type != MT_NONE) - { + if (!io.MouseDown[0]) + { + gContext.mbUsing = false; + } + + type = gContext.mCurrentOperation; + } + else + { + // find new possible way to move + vec_t gizmoHitProportion; + type = gContext.mbOverGizmoHotspot ? MT_NONE : GetMoveType(op, &gizmoHitProportion); + gContext.mbOverGizmoHotspot |= type != MT_NONE; + if (type != MT_NONE) + { #if IMGUI_VERSION_NUM >= 18723 - ImGui::SetNextFrameWantCaptureMouse(true); + ImGui::SetNextFrameWantCaptureMouse(true); #else - ImGui::CaptureMouseFromApp(); + ImGui::CaptureMouseFromApp(); #endif + } + if (CanActivate() && type != MT_NONE) + { + gContext.mbUsing = true; + gContext.mEditingID = gContext.GetCurrentID(); + gContext.mCurrentOperation = type; + vec_t movePlanNormal[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, + gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, + -gContext.mCameraDir }; + + vec_t cameraToModelNormalized = Normalized(gContext.mModel.v.position - gContext.mCameraEye); + for (unsigned int i = 0; i < 3; i++) + { + vec_t orthoVector = Cross(movePlanNormal[i], cameraToModelNormalized); + movePlanNormal[i].Cross(orthoVector); + movePlanNormal[i].Normalize(); } - if (CanActivate() && type != MT_NONE) - { - gContext.mbUsing = true; - gContext.mEditingID = gContext.GetCurrentID(); - gContext.mCurrentOperation = type; - vec_t movePlanNormal[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, gContext.mModel.v.right, - gContext.mModel.v.up, gContext.mModel.v.dir, -gContext.mCameraDir }; - - vec_t cameraToModelNormalized = Normalized(gContext.mModel.v.position - gContext.mCameraEye); - for (unsigned int i = 0; i < 3; i++) - { - vec_t orthoVector = Cross(movePlanNormal[i], cameraToModelNormalized); - movePlanNormal[i].Cross(orthoVector); - movePlanNormal[i].Normalize(); - } - // pickup plan - gContext.mTranslationPlan = BuildPlan(gContext.mModel.v.position, movePlanNormal[type - MT_MOVE_X]); - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); - gContext.mTranslationPlanOrigin = gContext.mRayOrigin + gContext.mRayVector * len; - gContext.mMatrixOrigin = gContext.mModel.v.position; - - gContext.mRelativeOrigin = (gContext.mTranslationPlanOrigin - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor); - } - } - return modified; - } - - static bool HandleScale(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) - { - if ((!Intersects(op, SCALE) && !Intersects(op, SCALEU)) || type != MT_NONE || !gContext.mbMouseOver) - { - return false; - } - ImGuiIO& io = ImGui::GetIO(); - bool modified = false; - - if (!gContext.mbUsing) - { - // find new possible way to scale - type = gContext.mbOverGizmoHotspot ? MT_NONE : GetScaleType(op); - gContext.mbOverGizmoHotspot |= type != MT_NONE; + // pickup plan + gContext.mTranslationPlan = BuildPlan(gContext.mModel.v.position, movePlanNormal[type - MT_MOVE_X]); + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + gContext.mTranslationPlanOrigin = gContext.mRayOrigin + gContext.mRayVector * len; + gContext.mMatrixOrigin = gContext.mModel.v.position; + + gContext.mRelativeOrigin = (gContext.mTranslationPlanOrigin - gContext.mModel.v.position) * (1.f / gContext.mScreenFactor); + } + } + return modified; + } + + static bool HandleScale(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) + { + if((!Intersects(op, SCALE) && !Intersects(op, SCALEU)) || type != MT_NONE || !gContext.mbMouseOver) + { + return false; + } + ImGuiIO& io = ImGui::GetIO(); + bool modified = false; + + if (!gContext.mbUsing) + { + // find new possible way to scale + type = gContext.mbOverGizmoHotspot ? MT_NONE : GetScaleType(op); + gContext.mbOverGizmoHotspot |= type != MT_NONE; + + if (type != MT_NONE) + { +#if IMGUI_VERSION_NUM >= 18723 + ImGui::SetNextFrameWantCaptureMouse(true); +#else + ImGui::CaptureMouseFromApp(); +#endif + } + if (CanActivate() && type != MT_NONE) + { + gContext.mbUsing = true; + gContext.mEditingID = gContext.GetCurrentID(); + gContext.mCurrentOperation = type; + const vec_t movePlanNormal[] = { gContext.mModelLocal.v.up, gContext.mModelLocal.v.dir, gContext.mModelLocal.v.right, gContext.mModelLocal.v.dir, gContext.mModelLocal.v.up, gContext.mModelLocal.v.right, -gContext.mCameraDir }; + // pickup plan - if (type != MT_NONE) - { + gContext.mTranslationPlan = BuildPlan(gContext.mModelLocal.v.position, movePlanNormal[type - MT_SCALE_X]); + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + gContext.mTranslationPlanOrigin = gContext.mRayOrigin + gContext.mRayVector * len; + gContext.mMatrixOrigin = gContext.mModelLocal.v.position; + gContext.mScale.Set(1.f, 1.f, 1.f); + gContext.mRelativeOrigin = (gContext.mTranslationPlanOrigin - gContext.mModelLocal.v.position) * (1.f / gContext.mScreenFactor); + gContext.mScaleValueOrigin = makeVect(gContext.mModelSource.v.right.Length(), gContext.mModelSource.v.up.Length(), gContext.mModelSource.v.dir.Length()); + gContext.mSaveMousePosx = io.MousePos.x; + } + } + // scale + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsScaleType(gContext.mCurrentOperation)) + { #if IMGUI_VERSION_NUM >= 18723 - ImGui::SetNextFrameWantCaptureMouse(true); + ImGui::SetNextFrameWantCaptureMouse(true); #else - ImGui::CaptureMouseFromApp(); + ImGui::CaptureMouseFromApp(); #endif - } - if (CanActivate() && type != MT_NONE) - { - gContext.mbUsing = true; - gContext.mEditingID = gContext.GetCurrentID(); - gContext.mCurrentOperation = type; - const vec_t movePlanNormal[] = { gContext.mModelLocal.v.up, gContext.mModelLocal.v.dir, gContext.mModelLocal.v.right, - gContext.mModelLocal.v.dir, gContext.mModelLocal.v.up, gContext.mModelLocal.v.right, - -gContext.mCameraDir }; - // pickup plan - - gContext.mTranslationPlan = BuildPlan(gContext.mModelLocal.v.position, movePlanNormal[type - MT_SCALE_X]); - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); - gContext.mTranslationPlanOrigin = gContext.mRayOrigin + gContext.mRayVector * len; - gContext.mMatrixOrigin = gContext.mModelLocal.v.position; - gContext.mScale.Set(1.f, 1.f, 1.f); - gContext.mRelativeOrigin = - (gContext.mTranslationPlanOrigin - gContext.mModelLocal.v.position) * (1.f / gContext.mScreenFactor); - gContext.mScaleValueOrigin = makeVect( - gContext.mModelSource.v.right.Length(), gContext.mModelSource.v.up.Length(), gContext.mModelSource.v.dir.Length()); - gContext.mSaveMousePosx = io.MousePos.x; - } - } - // scale - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsScaleType(gContext.mCurrentOperation)) - { + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; + vec_t newOrigin = newPos - gContext.mRelativeOrigin * gContext.mScreenFactor; + vec_t delta = newOrigin - gContext.mModelLocal.v.position; + + // 1 axis constraint + if (gContext.mCurrentOperation >= MT_SCALE_X && gContext.mCurrentOperation <= MT_SCALE_Z) + { + int axisIndex = gContext.mCurrentOperation - MT_SCALE_X; + const vec_t& axisValue = *(vec_t*)&gContext.mModelLocal.m[axisIndex]; + float lengthOnAxis = Dot(axisValue, delta); + delta = axisValue * lengthOnAxis; + + vec_t baseVector = gContext.mTranslationPlanOrigin - gContext.mModelLocal.v.position; + float ratio = Dot(axisValue, baseVector + delta) / Dot(axisValue, baseVector); + + gContext.mScale[axisIndex] = max(ratio, 0.001f); + } + else + { + float scaleDelta = (io.MousePos.x - gContext.mSaveMousePosx) * 0.01f; + gContext.mScale.Set(max(1.f + scaleDelta, 0.001f)); + } + + // snap + if (snap) + { + float scaleSnap[] = { snap[0], snap[0], snap[0] }; + ComputeSnap(gContext.mScale, scaleSnap); + } + + // no 0 allowed + for (int i = 0; i < 3; i++) + gContext.mScale[i] = max(gContext.mScale[i], 0.001f); + + if (gContext.mScaleLast != gContext.mScale) + { + modified = true; + } + gContext.mScaleLast = gContext.mScale; + + // compute matrix & delta + matrix_t deltaMatrixScale; + deltaMatrixScale.Scale(gContext.mScale * gContext.mScaleValueOrigin); + + matrix_t res = deltaMatrixScale * gContext.mModelLocal; + *(matrix_t*)matrix = res; + + if (deltaMatrix) + { + vec_t deltaScale = gContext.mScale * gContext.mScaleValueOrigin; + + vec_t originalScaleDivider; + originalScaleDivider.x = 1 / gContext.mModelScaleOrigin.x; + originalScaleDivider.y = 1 / gContext.mModelScaleOrigin.y; + originalScaleDivider.z = 1 / gContext.mModelScaleOrigin.z; + + deltaScale = deltaScale * originalScaleDivider; + + deltaMatrixScale.Scale(deltaScale); + memcpy(deltaMatrix, deltaMatrixScale.m16, sizeof(float) * 16); + } + + if (!io.MouseDown[0]) + { + gContext.mbUsing = false; + gContext.mScale.Set(1.f, 1.f, 1.f); + } + + type = gContext.mCurrentOperation; + } + return modified; + } + + static bool HandleRotation(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) + { + if(!Intersects(op, ROTATE) || type != MT_NONE || !gContext.mbMouseOver) + { + return false; + } + ImGuiIO& io = ImGui::GetIO(); + bool applyRotationLocaly = gContext.mMode == LOCAL; + bool modified = false; + + if (!gContext.mbUsing) + { + type = gContext.mbOverGizmoHotspot ? MT_NONE : GetRotateType(op); + gContext.mbOverGizmoHotspot |= type != MT_NONE; + + if (type != MT_NONE) + { #if IMGUI_VERSION_NUM >= 18723 ImGui::SetNextFrameWantCaptureMouse(true); #else ImGui::CaptureMouseFromApp(); #endif - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); - vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; - vec_t newOrigin = newPos - gContext.mRelativeOrigin * gContext.mScreenFactor; - vec_t delta = newOrigin - gContext.mModelLocal.v.position; - - // 1 axis constraint - if (gContext.mCurrentOperation >= MT_SCALE_X && gContext.mCurrentOperation <= MT_SCALE_Z) + } + + if (type == MT_ROTATE_SCREEN) + { + applyRotationLocaly = true; + } + + if (CanActivate() && type != MT_NONE) + { + gContext.mbUsing = true; + gContext.mEditingID = gContext.GetCurrentID(); + gContext.mCurrentOperation = type; + const vec_t rotatePlanNormal[] = { gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, -gContext.mCameraDir }; + // pickup plan + if (applyRotationLocaly) { - int axisIndex = gContext.mCurrentOperation - MT_SCALE_X; - const vec_t& axisValue = *(vec_t*)&gContext.mModelLocal.m[axisIndex]; - float lengthOnAxis = Dot(axisValue, delta); - delta = axisValue * lengthOnAxis; - - vec_t baseVector = gContext.mTranslationPlanOrigin - gContext.mModelLocal.v.position; - float ratio = Dot(axisValue, baseVector + delta) / Dot(axisValue, baseVector); - - gContext.mScale[axisIndex] = max(ratio, 0.001f); + gContext.mTranslationPlan = BuildPlan(gContext.mModel.v.position, rotatePlanNormal[type - MT_ROTATE_X]); } else { - float scaleDelta = (io.MousePos.x - gContext.mSaveMousePosx) * 0.01f; - gContext.mScale.Set(max(1.f + scaleDelta, 0.001f)); - } - - // snap - if (snap) - { - float scaleSnap[] = { snap[0], snap[0], snap[0] }; - ComputeSnap(gContext.mScale, scaleSnap); + gContext.mTranslationPlan = BuildPlan(gContext.mModelSource.v.position, directionUnary[type - MT_ROTATE_X]); } - // no 0 allowed - for (int i = 0; i < 3; i++) - gContext.mScale[i] = max(gContext.mScale[i], 0.001f); - - if (gContext.mScaleLast != gContext.mScale) - { - modified = true; - } - gContext.mScaleLast = gContext.mScale; - - // compute matrix & delta - matrix_t deltaMatrixScale; - deltaMatrixScale.Scale(gContext.mScale * gContext.mScaleValueOrigin); - - matrix_t res = deltaMatrixScale * gContext.mModelLocal; - *(matrix_t*)matrix = res; - - if (deltaMatrix) - { - vec_t deltaScale = gContext.mScale * gContext.mScaleValueOrigin; - - vec_t originalScaleDivider; - originalScaleDivider.x = 1 / gContext.mModelScaleOrigin.x; - originalScaleDivider.y = 1 / gContext.mModelScaleOrigin.y; - originalScaleDivider.z = 1 / gContext.mModelScaleOrigin.z; - - deltaScale = deltaScale * originalScaleDivider; - - deltaMatrixScale.Scale(deltaScale); - memcpy(deltaMatrix, deltaMatrixScale.m16, sizeof(float) * 16); - } - - if (!io.MouseDown[0]) - { - gContext.mbUsing = false; - gContext.mScale.Set(1.f, 1.f, 1.f); - } - - type = gContext.mCurrentOperation; - } - return modified; - } - - static bool HandleRotation(float* matrix, float* deltaMatrix, OPERATION op, int& type, const float* snap) - { - if (!Intersects(op, ROTATE) || type != MT_NONE || !gContext.mbMouseOver) - { - return false; - } - ImGuiIO& io = ImGui::GetIO(); - bool applyRotationLocaly = gContext.mMode == LOCAL; - bool modified = false; - - if (!gContext.mbUsing) - { - type = gContext.mbOverGizmoHotspot ? MT_NONE : GetRotateType(op); - gContext.mbOverGizmoHotspot |= type != MT_NONE; - - if (type != MT_NONE) - { + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); + vec_t localPos = gContext.mRayOrigin + gContext.mRayVector * len - gContext.mModel.v.position; + gContext.mRotationVectorSource = Normalized(localPos); + gContext.mRotationAngleOrigin = ComputeAngleOnPlan(); + } + } + + // rotation + if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsRotateType(gContext.mCurrentOperation)) + { #if IMGUI_VERSION_NUM >= 18723 - ImGui::SetNextFrameWantCaptureMouse(true); + ImGui::SetNextFrameWantCaptureMouse(true); #else - ImGui::CaptureMouseFromApp(); + ImGui::CaptureMouseFromApp(); #endif + gContext.mRotationAngle = ComputeAngleOnPlan(); + if (snap) + { + float snapInRadian = snap[0] * DEG2RAD; + ComputeSnap(&gContext.mRotationAngle, snapInRadian); + } + vec_t rotationAxisLocalSpace; + + rotationAxisLocalSpace.TransformVector(makeVect(gContext.mTranslationPlan.x, gContext.mTranslationPlan.y, gContext.mTranslationPlan.z, 0.f), gContext.mModelInverse); + rotationAxisLocalSpace.Normalize(); + + matrix_t deltaRotation; + deltaRotation.RotationAxis(rotationAxisLocalSpace, gContext.mRotationAngle - gContext.mRotationAngleOrigin); + if (gContext.mRotationAngle != gContext.mRotationAngleOrigin) + { + modified = true; + } + gContext.mRotationAngleOrigin = gContext.mRotationAngle; + + matrix_t scaleOrigin; + scaleOrigin.Scale(gContext.mModelScaleOrigin); + + if (applyRotationLocaly) + { + *(matrix_t*)matrix = scaleOrigin * deltaRotation * gContext.mModelLocal; + } + else + { + matrix_t res = gContext.mModelSource; + res.v.position.Set(0.f); + + *(matrix_t*)matrix = res * deltaRotation; + ((matrix_t*)matrix)->v.position = gContext.mModelSource.v.position; + } + + if (deltaMatrix) + { + *(matrix_t*)deltaMatrix = gContext.mModelInverse * deltaRotation * gContext.mModel; + } + + if (!io.MouseDown[0]) + { + gContext.mbUsing = false; + gContext.mEditingID = -1; + } + type = gContext.mCurrentOperation; + } + return modified; + } + + void DecomposeMatrixToComponents(const float* matrix, float* translation, float* rotation, float* scale) + { + matrix_t mat = *(matrix_t*)matrix; + + scale[0] = mat.v.right.Length(); + scale[1] = mat.v.up.Length(); + scale[2] = mat.v.dir.Length(); + + mat.OrthoNormalize(); + + rotation[0] = RAD2DEG * atan2f(mat.m[1][2], mat.m[2][2]); + rotation[1] = RAD2DEG * atan2f(-mat.m[0][2], sqrtf(mat.m[1][2] * mat.m[1][2] + mat.m[2][2] * mat.m[2][2])); + rotation[2] = RAD2DEG * atan2f(mat.m[0][1], mat.m[0][0]); + + translation[0] = mat.v.position.x; + translation[1] = mat.v.position.y; + translation[2] = mat.v.position.z; + } + + void RecomposeMatrixFromComponents(const float* translation, const float* rotation, const float* scale, float* matrix) + { + matrix_t& mat = *(matrix_t*)matrix; + + matrix_t rot[3]; + for (int i = 0; i < 3; i++) + { + rot[i].RotationAxis(directionUnary[i], rotation[i] * DEG2RAD); + } + + mat = rot[0] * rot[1] * rot[2]; + + float validScale[3]; + for (int i = 0; i < 3; i++) + { + if (fabsf(scale[i]) < FLT_EPSILON) + { + validScale[i] = 0.001f; + } + else + { + validScale[i] = scale[i]; + } + } + mat.v.right *= validScale[0]; + mat.v.up *= validScale[1]; + mat.v.dir *= validScale[2]; + mat.v.position.Set(translation[0], translation[1], translation[2], 1.f); + } + + void SetAlternativeWindow(ImGuiWindow* window) + { + gContext.mAlternativeWindow = window; + } + + void SetID(int id) + { + if (gContext.mIDStack.empty()) + { + gContext.mIDStack.push_back(-1); + } + gContext.mIDStack.back() = id; + } + + ImGuiID GetID(const char* str, const char* str_end) + { + ImGuiID seed = gContext.GetCurrentID(); + ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); + return id; + } + + ImGuiID GetID(const char* str) + { + return GetID(str, nullptr); + } + + ImGuiID GetID(const void* ptr) + { + ImGuiID seed = gContext.GetCurrentID(); + ImGuiID id = ImHashData(&ptr, sizeof(void*), seed); + return id; + } + + ImGuiID GetID(int n) + { + ImGuiID seed = gContext.GetCurrentID(); + ImGuiID id = ImHashData(&n, sizeof(n), seed); + return id; + } + + void PushID(const char* str_id) + { + ImGuiID id = GetID(str_id); + gContext.mIDStack.push_back(id); + } + + void PushID(const char* str_id_begin, const char* str_id_end) + { + ImGuiID id = GetID(str_id_begin, str_id_end); + gContext.mIDStack.push_back(id); + } + + void PushID(const void* ptr_id) + { + ImGuiID id = GetID(ptr_id); + gContext.mIDStack.push_back(id); + } + + void PushID(int int_id) + { + ImGuiID id = GetID(int_id); + gContext.mIDStack.push_back(id); + } + + void PopID() + { + IM_ASSERT(gContext.mIDStack.Size > 1); // Too many PopID(), or could be popping in a wrong/different window? + gContext.mIDStack.pop_back(); + } + + void AllowAxisFlip(bool value) + { + gContext.mAllowAxisFlip = value; + } + + void SetAxisLimit(float value) + { + gContext.mAxisLimit=value; + } + + void SetAxisMask(bool x, bool y, bool z) + { + gContext.mAxisMask = (x ? 1 : 0) + (y ? 2 : 0) + (z ? 4 : 0); + } + + void SetPlaneLimit(float value) + { + gContext.mPlaneLimit = value; + } + + bool IsOver(float* position, float pixelRadius) + { + const ImGuiIO& io = ImGui::GetIO(); + + float radius = sqrtf((ImLengthSqr(worldToPos({ position[0], position[1], position[2], 0.0f }, gContext.mViewProjection) - io.MousePos))); + return radius < pixelRadius; + } + + bool Manipulate(const float* view, const float* projection, OPERATION operation, MODE mode, float* matrix, float* deltaMatrix, const float* snap, const float* localBounds, const float* boundsSnap) + { + gContext.mDrawList->PushClipRect (ImVec2 (gContext.mX, gContext.mY), ImVec2 (gContext.mX + gContext.mWidth, gContext.mY + gContext.mHeight), false); + + // Scale is always local or matrix will be skewed when applying world scale or oriented matrix + ComputeContext(view, projection, matrix, (operation & SCALE) ? LOCAL : mode); + + // set delta to identity + if (deltaMatrix) + { + ((matrix_t*)deltaMatrix)->SetToIdentity(); + } + + // behind camera + vec_t camSpacePosition; + camSpacePosition.TransformPoint(makeVect(0.f, 0.f, 0.f), gContext.mMVP); + if (!gContext.mIsOrthographic && camSpacePosition.z < 0.001f && !gContext.mbUsing) + { + return false; + } + + // -- + int type = MT_NONE; + bool manipulated = false; + if (gContext.mbEnable) + { + if (!gContext.mbUsingBounds) + { + manipulated = HandleTranslation(matrix, deltaMatrix, operation, type, snap) || + HandleScale(matrix, deltaMatrix, operation, type, snap) || + HandleRotation(matrix, deltaMatrix, operation, type, snap); + } + } + + if (localBounds && !gContext.mbUsing) + { + HandleAndDrawLocalBounds(localBounds, (matrix_t*)matrix, boundsSnap, operation); + } + + gContext.mOperation = operation; + if (!gContext.mbUsingBounds) + { + DrawRotationGizmo(operation, type); + DrawTranslationGizmo(operation, type); + DrawScaleGizmo(operation, type); + DrawScaleUniveralGizmo(operation, type); + } + + gContext.mDrawList->PopClipRect (); + return manipulated; + } + + void SetGizmoSizeClipSpace(float value) + { + gContext.mGizmoSizeClipSpace = value; + } + + /////////////////////////////////////////////////////////////////////////////////////////////////// + void ComputeFrustumPlanes(vec_t* frustum, const float* clip) + { + frustum[0].x = clip[3] - clip[0]; + frustum[0].y = clip[7] - clip[4]; + frustum[0].z = clip[11] - clip[8]; + frustum[0].w = clip[15] - clip[12]; + + frustum[1].x = clip[3] + clip[0]; + frustum[1].y = clip[7] + clip[4]; + frustum[1].z = clip[11] + clip[8]; + frustum[1].w = clip[15] + clip[12]; + + frustum[2].x = clip[3] + clip[1]; + frustum[2].y = clip[7] + clip[5]; + frustum[2].z = clip[11] + clip[9]; + frustum[2].w = clip[15] + clip[13]; + + frustum[3].x = clip[3] - clip[1]; + frustum[3].y = clip[7] - clip[5]; + frustum[3].z = clip[11] - clip[9]; + frustum[3].w = clip[15] - clip[13]; + + frustum[4].x = clip[3] - clip[2]; + frustum[4].y = clip[7] - clip[6]; + frustum[4].z = clip[11] - clip[10]; + frustum[4].w = clip[15] - clip[14]; + + frustum[5].x = clip[3] + clip[2]; + frustum[5].y = clip[7] + clip[6]; + frustum[5].z = clip[11] + clip[10]; + frustum[5].w = clip[15] + clip[14]; + + for (int i = 0; i < 6; i++) + { + frustum[i].Normalize(); + } + } + + void DrawCubes(const float* view, const float* projection, const float* matrices, int matrixCount) + { + matrix_t viewInverse; + viewInverse.Inverse(*(matrix_t*)view); + + struct CubeFace + { + float z; + ImVec2 faceCoordsScreen[4]; + ImU32 color; + }; + CubeFace* faces = (CubeFace*)_malloca(sizeof(CubeFace) * matrixCount * 6); + + if (!faces) + { + return; + } + + vec_t frustum[6]; + matrix_t viewProjection = *(matrix_t*)view * *(matrix_t*)projection; + ComputeFrustumPlanes(frustum, viewProjection.m16); + + int cubeFaceCount = 0; + for (int cube = 0; cube < matrixCount; cube++) + { + const float* matrix = &matrices[cube * 16]; + + matrix_t res = *(matrix_t*)matrix * *(matrix_t*)view * *(matrix_t*)projection; + + for (int iFace = 0; iFace < 6; iFace++) + { + const int normalIndex = (iFace % 3); + const int perpXIndex = (normalIndex + 1) % 3; + const int perpYIndex = (normalIndex + 2) % 3; + const float invert = (iFace > 2) ? -1.f : 1.f; + + const vec_t faceCoords[4] = { directionUnary[normalIndex] + directionUnary[perpXIndex] + directionUnary[perpYIndex], + directionUnary[normalIndex] + directionUnary[perpXIndex] - directionUnary[perpYIndex], + directionUnary[normalIndex] - directionUnary[perpXIndex] - directionUnary[perpYIndex], + directionUnary[normalIndex] - directionUnary[perpXIndex] + directionUnary[perpYIndex], + }; + + // clipping + /* + bool skipFace = false; + for (unsigned int iCoord = 0; iCoord < 4; iCoord++) + { + vec_t camSpacePosition; + camSpacePosition.TransformPoint(faceCoords[iCoord] * 0.5f * invert, res); + if (camSpacePosition.z < 0.001f) + { + skipFace = true; + break; + } } - - if (type == MT_ROTATE_SCREEN) + if (skipFace) { - applyRotationLocaly = true; + continue; } + */ + vec_t centerPosition, centerPositionVP; + centerPosition.TransformPoint(directionUnary[normalIndex] * 0.5f * invert, *(matrix_t*)matrix); + centerPositionVP.TransformPoint(directionUnary[normalIndex] * 0.5f * invert, res); - if (CanActivate() && type != MT_NONE) + bool inFrustum = true; + for (int iFrustum = 0; iFrustum < 6; iFrustum++) { - gContext.mbUsing = true; - gContext.mEditingID = gContext.GetCurrentID(); - gContext.mCurrentOperation = type; - const vec_t rotatePlanNormal[] = { - gContext.mModel.v.right, gContext.mModel.v.up, gContext.mModel.v.dir, -gContext.mCameraDir - }; - // pickup plan - if (applyRotationLocaly) - { - gContext.mTranslationPlan = BuildPlan(gContext.mModel.v.position, rotatePlanNormal[type - MT_ROTATE_X]); - } - else - { - gContext.mTranslationPlan = BuildPlan(gContext.mModelSource.v.position, directionUnary[type - MT_ROTATE_X]); - } - - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); - vec_t localPos = gContext.mRayOrigin + gContext.mRayVector * len - gContext.mModel.v.position; - gContext.mRotationVectorSource = Normalized(localPos); - gContext.mRotationAngleOrigin = ComputeAngleOnPlan(); + float dist = DistanceToPlane(centerPosition, frustum[iFrustum]); + if (dist < 0.f) + { + inFrustum = false; + break; + } } - } - // rotation - if (gContext.mbUsing && (gContext.GetCurrentID() == gContext.mEditingID) && IsRotateType(gContext.mCurrentOperation)) - { -#if IMGUI_VERSION_NUM >= 18723 - ImGui::SetNextFrameWantCaptureMouse(true); -#else - ImGui::CaptureMouseFromApp(); -#endif - gContext.mRotationAngle = ComputeAngleOnPlan(); - if (snap) - { - float snapInRadian = snap[0] * DEG2RAD; - ComputeSnap(&gContext.mRotationAngle, snapInRadian); + if (!inFrustum) + { + continue; + } + CubeFace& cubeFace = faces[cubeFaceCount]; + + // 3D->2D + //ImVec2 faceCoordsScreen[4]; + for (unsigned int iCoord = 0; iCoord < 4; iCoord++) + { + cubeFace.faceCoordsScreen[iCoord] = worldToPos(faceCoords[iCoord] * 0.5f * invert, res); + } + + ImU32 directionColor = GetColorU32(DIRECTION_X + normalIndex); + cubeFace.color = directionColor | IM_COL32(0x80, 0x80, 0x80, 0); + + cubeFace.z = centerPositionVP.z / centerPositionVP.w; + cubeFaceCount++; + } + } + qsort(faces, cubeFaceCount, sizeof(CubeFace), [](void const* _a, void const* _b) { + CubeFace* a = (CubeFace*)_a; + CubeFace* b = (CubeFace*)_b; + if (a->z < b->z) + { + return 1; + } + return -1; + }); + // draw face with lighter color + for (int iFace = 0; iFace < cubeFaceCount; iFace++) + { + const CubeFace& cubeFace = faces[iFace]; + gContext.mDrawList->AddConvexPolyFilled(cubeFace.faceCoordsScreen, 4, cubeFace.color); + } + + _freea(faces); + } + + void DrawGrid(const float* view, const float* projection, const float* matrix, const float gridSize) + { + matrix_t viewProjection = *(matrix_t*)view * *(matrix_t*)projection; + vec_t frustum[6]; + ComputeFrustumPlanes(frustum, viewProjection.m16); + matrix_t res = *(matrix_t*)matrix * viewProjection; + + for (float f = -gridSize; f <= gridSize; f += 1.f) + { + for (int dir = 0; dir < 2; dir++) + { + vec_t ptA = makeVect(dir ? -gridSize : f, 0.f, dir ? f : -gridSize); + vec_t ptB = makeVect(dir ? gridSize : f, 0.f, dir ? f : gridSize); + bool visible = true; + for (int i = 0; i < 6; i++) + { + float dA = DistanceToPlane(ptA, frustum[i]); + float dB = DistanceToPlane(ptB, frustum[i]); + if (dA < 0.f && dB < 0.f) + { + visible = false; + break; + } + if (dA > 0.f && dB > 0.f) + { + continue; + } + if (dA < 0.f) + { + float len = fabsf(dA - dB); + float t = fabsf(dA) / len; + ptA.Lerp(ptB, t); + } + if (dB < 0.f) + { + float len = fabsf(dB - dA); + float t = fabsf(dB) / len; + ptB.Lerp(ptA, t); + } } - vec_t rotationAxisLocalSpace; + if (visible) + { + ImU32 col = IM_COL32(0x80, 0x80, 0x80, 0xFF); + col = (fmodf(fabsf(f), 10.f) < FLT_EPSILON) ? IM_COL32(0x90, 0x90, 0x90, 0xFF) : col; + col = (fabsf(f) < FLT_EPSILON) ? IM_COL32(0x40, 0x40, 0x40, 0xFF): col; + + float thickness = 1.f; + thickness = (fmodf(fabsf(f), 10.f) < FLT_EPSILON) ? 1.5f : thickness; + thickness = (fabsf(f) < FLT_EPSILON) ? 2.3f : thickness; + + gContext.mDrawList->AddLine(worldToPos(ptA, res), worldToPos(ptB, res), col, thickness); + } + } + } + } + + void ViewManipulate(float* view, const float* projection, OPERATION operation, MODE mode, float* matrix, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor) + { + // Scale is always local or matrix will be skewed when applying world scale or oriented matrix + ComputeContext(view, projection, matrix, (operation & SCALE) ? LOCAL : mode); + ViewManipulate(view, length, position, size, backgroundColor); + } + + void ViewManipulate(float* view, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor) + { + static bool isDraging = false; + static bool isClicking = false; + static vec_t interpolationUp; + static vec_t interpolationDir; + static int interpolationFrames = 0; + const vec_t referenceUp = makeVect(0.f, 1.f, 0.f); + + matrix_t svgView, svgProjection; + svgView = gContext.mViewMat; + svgProjection = gContext.mProjectionMat; + + ImGuiIO& io = ImGui::GetIO(); + gContext.mDrawList->AddRectFilled(position, position + size, backgroundColor); + matrix_t viewInverse; + viewInverse.Inverse(*(matrix_t*)view); + + const vec_t camTarget = viewInverse.v.position - viewInverse.v.dir * length; + + // view/projection matrices + const float distance = 3.f; + matrix_t cubeProjection, cubeView; + float fov = acosf(distance / (sqrtf(distance * distance + 3.f))) * RAD2DEG; + Perspective(fov / sqrtf(2.f), size.x / size.y, 0.01f, 1000.f, cubeProjection.m16); + + vec_t dir = makeVect(viewInverse.m[2][0], viewInverse.m[2][1], viewInverse.m[2][2]); + vec_t up = makeVect(viewInverse.m[1][0], viewInverse.m[1][1], viewInverse.m[1][2]); + vec_t eye = dir * distance; + vec_t zero = makeVect(0.f, 0.f); + LookAt(&eye.x, &zero.x, &up.x, cubeView.m16); + + // set context + gContext.mViewMat = cubeView; + gContext.mProjectionMat = cubeProjection; + ComputeCameraRay(gContext.mRayOrigin, gContext.mRayVector, position, size); + + const matrix_t res = cubeView * cubeProjection; + + // panels + static const ImVec2 panelPosition[9] = { ImVec2(0.75f,0.75f), ImVec2(0.25f, 0.75f), ImVec2(0.f, 0.75f), + ImVec2(0.75f, 0.25f), ImVec2(0.25f, 0.25f), ImVec2(0.f, 0.25f), + ImVec2(0.75f, 0.f), ImVec2(0.25f, 0.f), ImVec2(0.f, 0.f) }; + + static const ImVec2 panelSize[9] = { ImVec2(0.25f,0.25f), ImVec2(0.5f, 0.25f), ImVec2(0.25f, 0.25f), + ImVec2(0.25f, 0.5f), ImVec2(0.5f, 0.5f), ImVec2(0.25f, 0.5f), + ImVec2(0.25f, 0.25f), ImVec2(0.5f, 0.25f), ImVec2(0.25f, 0.25f) }; + + // tag faces + bool boxes[27]{}; + static int overBox = -1; + for (int iPass = 0; iPass < 2; iPass++) + { + for (int iFace = 0; iFace < 6; iFace++) + { + const int normalIndex = (iFace % 3); + const int perpXIndex = (normalIndex + 1) % 3; + const int perpYIndex = (normalIndex + 2) % 3; + const float invert = (iFace > 2) ? -1.f : 1.f; + const vec_t indexVectorX = directionUnary[perpXIndex] * invert; + const vec_t indexVectorY = directionUnary[perpYIndex] * invert; + const vec_t boxOrigin = directionUnary[normalIndex] * -invert - indexVectorX - indexVectorY; + + // plan local space + const vec_t n = directionUnary[normalIndex] * invert; + vec_t viewSpaceNormal = n; + vec_t viewSpacePoint = n * 0.5f; + viewSpaceNormal.TransformVector(cubeView); + viewSpaceNormal.Normalize(); + viewSpacePoint.TransformPoint(cubeView); + const vec_t viewSpaceFacePlan = BuildPlan(viewSpacePoint, viewSpaceNormal); + + // back face culling + if (viewSpaceFacePlan.w > 0.f) + { + continue; + } + + const vec_t facePlan = BuildPlan(n * 0.5f, n); + + const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, facePlan); + vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len - (n * 0.5f); + + float localx = Dot(directionUnary[perpXIndex], posOnPlan) * invert + 0.5f; + float localy = Dot(directionUnary[perpYIndex], posOnPlan) * invert + 0.5f; + + // panels + const vec_t dx = directionUnary[perpXIndex]; + const vec_t dy = directionUnary[perpYIndex]; + const vec_t origin = directionUnary[normalIndex] - dx - dy; + for (int iPanel = 0; iPanel < 9; iPanel++) + { + vec_t boxCoord = boxOrigin + indexVectorX * float(iPanel % 3) + indexVectorY * float(iPanel / 3) + makeVect(1.f, 1.f, 1.f); + const ImVec2 p = panelPosition[iPanel] * 2.f; + const ImVec2 s = panelSize[iPanel] * 2.f; + ImVec2 faceCoordsScreen[4]; + vec_t panelPos[4] = { dx * p.x + dy * p.y, + dx * p.x + dy * (p.y + s.y), + dx * (p.x + s.x) + dy * (p.y + s.y), + dx * (p.x + s.x) + dy * p.y }; + + for (unsigned int iCoord = 0; iCoord < 4; iCoord++) + { + faceCoordsScreen[iCoord] = worldToPos((panelPos[iCoord] + origin) * 0.5f * invert, res, position, size); + } - rotationAxisLocalSpace.TransformVector( - makeVect(gContext.mTranslationPlan.x, gContext.mTranslationPlan.y, gContext.mTranslationPlan.z, 0.f), - gContext.mModelInverse); - rotationAxisLocalSpace.Normalize(); + const ImVec2 panelCorners[2] = { panelPosition[iPanel], panelPosition[iPanel] + panelSize[iPanel] }; + bool insidePanel = localx > panelCorners[0].x && localx < panelCorners[1].x && localy > panelCorners[0].y && localy < panelCorners[1].y; + int boxCoordInt = int(boxCoord.x * 9.f + boxCoord.y * 3.f + boxCoord.z); + IM_ASSERT(boxCoordInt < 27); + boxes[boxCoordInt] |= insidePanel && (!isDraging) && gContext.mbMouseOver; - matrix_t deltaRotation; - deltaRotation.RotationAxis(rotationAxisLocalSpace, gContext.mRotationAngle - gContext.mRotationAngleOrigin); - if (gContext.mRotationAngle != gContext.mRotationAngleOrigin) - { - modified = true; + // draw face with lighter color + if (iPass) + { + ImU32 directionColor = GetColorU32(DIRECTION_X + normalIndex); + gContext.mDrawList->AddConvexPolyFilled(faceCoordsScreen, 4, (directionColor | IM_COL32(0x80, 0x80, 0x80, 0x80)) | (gContext.mIsViewManipulatorHovered ? IM_COL32(0x08, 0x08, 0x08, 0) : 0)); + if (boxes[boxCoordInt]) + { + gContext.mDrawList->AddConvexPolyFilled(faceCoordsScreen, 4, IM_COL32(0xF0, 0xA0, 0x60, 0x80)); + + if (io.MouseDown[0] && !isClicking && !isDraging && GImGui->ActiveId == 0) { + overBox = boxCoordInt; + isClicking = true; + isDraging = true; + } + } + } } - gContext.mRotationAngleOrigin = gContext.mRotationAngle; - - matrix_t scaleOrigin; - scaleOrigin.Scale(gContext.mModelScaleOrigin); - - if (applyRotationLocaly) - { - *(matrix_t*)matrix = scaleOrigin * deltaRotation * gContext.mModelLocal; + } + } + if (interpolationFrames) + { + interpolationFrames--; + vec_t newDir = viewInverse.v.dir; + newDir.Lerp(interpolationDir, 0.2f); + newDir.Normalize(); + + vec_t newUp = viewInverse.v.up; + newUp.Lerp(interpolationUp, 0.3f); + newUp.Normalize(); + newUp = interpolationUp; + vec_t newEye = camTarget + newDir * length; + LookAt(&newEye.x, &camTarget.x, &newUp.x, view); + } + gContext.mIsViewManipulatorHovered = gContext.mbMouseOver && ImRect(position, position + size).Contains(io.MousePos); + + if (io.MouseDown[0] && (fabsf(io.MouseDelta[0]) || fabsf(io.MouseDelta[1])) && isClicking) + { + isClicking = false; + } + + if (!io.MouseDown[0]) + { + if (isClicking) + { + // apply new view direction + int cx = overBox / 9; + int cy = (overBox - cx * 9) / 3; + int cz = overBox % 3; + interpolationDir = makeVect(1.f - (float)cx, 1.f - (float)cy, 1.f - (float)cz); + interpolationDir.Normalize(); + + if (fabsf(Dot(interpolationDir, referenceUp)) > 1.0f - 0.01f) + { + vec_t right = viewInverse.v.right; + if (fabsf(right.x) > fabsf(right.z)) + { + right.z = 0.f; + } + else + { + right.x = 0.f; + } + right.Normalize(); + interpolationUp = Cross(interpolationDir, right); + interpolationUp.Normalize(); } else { - matrix_t res = gContext.mModelSource; - res.v.position.Set(0.f); - - *(matrix_t*)matrix = res * deltaRotation; - ((matrix_t*)matrix)->v.position = gContext.mModelSource.v.position; + interpolationUp = referenceUp; } + interpolationFrames = 40; - if (deltaMatrix) - { - *(matrix_t*)deltaMatrix = gContext.mModelInverse * deltaRotation * gContext.mModel; - } + } + isClicking = false; + isDraging = false; + } - if (!io.MouseDown[0]) - { - gContext.mbUsing = false; - gContext.mEditingID = -1; - } - type = gContext.mCurrentOperation; - } - return modified; - } - - void DecomposeMatrixToComponents(const float* matrix, float* translation, float* rotation, float* scale) - { - matrix_t mat = *(matrix_t*)matrix; - - scale[0] = mat.v.right.Length(); - scale[1] = mat.v.up.Length(); - scale[2] = mat.v.dir.Length(); - - mat.OrthoNormalize(); - - rotation[0] = RAD2DEG * atan2f(mat.m[1][2], mat.m[2][2]); - rotation[1] = RAD2DEG * atan2f(-mat.m[0][2], sqrtf(mat.m[1][2] * mat.m[1][2] + mat.m[2][2] * mat.m[2][2])); - rotation[2] = RAD2DEG * atan2f(mat.m[0][1], mat.m[0][0]); - - translation[0] = mat.v.position.x; - translation[1] = mat.v.position.y; - translation[2] = mat.v.position.z; - } - - void RecomposeMatrixFromComponents(const float* translation, const float* rotation, const float* scale, float* matrix) - { - matrix_t& mat = *(matrix_t*)matrix; - - matrix_t rot[3]; - for (int i = 0; i < 3; i++) - { - rot[i].RotationAxis(directionUnary[i], rotation[i] * DEG2RAD); - } - - mat = rot[0] * rot[1] * rot[2]; - - float validScale[3]; - for (int i = 0; i < 3; i++) - { - if (fabsf(scale[i]) < FLT_EPSILON) - { - validScale[i] = 0.001f; - } - else - { - validScale[i] = scale[i]; - } - } - mat.v.right *= validScale[0]; - mat.v.up *= validScale[1]; - mat.v.dir *= validScale[2]; - mat.v.position.Set(translation[0], translation[1], translation[2], 1.f); - } - - void SetAlternativeWindow(ImGuiWindow* window) - { - gContext.mAlternativeWindow = window; - } - - void SetID(int id) - { - if (gContext.mIDStack.empty()) - { - gContext.mIDStack.push_back(-1); - } - gContext.mIDStack.back() = id; - } - - ImGuiID GetID(const char* str, const char* str_end) - { - ImGuiID seed = gContext.GetCurrentID(); - ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); - return id; - } - - ImGuiID GetID(const char* str) - { - return GetID(str, nullptr); - } - - ImGuiID GetID(const void* ptr) - { - ImGuiID seed = gContext.GetCurrentID(); - ImGuiID id = ImHashData(&ptr, sizeof(void*), seed); - return id; - } - - ImGuiID GetID(int n) - { - ImGuiID seed = gContext.GetCurrentID(); - ImGuiID id = ImHashData(&n, sizeof(n), seed); - return id; - } - - void PushID(const char* str_id) - { - ImGuiID id = GetID(str_id); - gContext.mIDStack.push_back(id); - } - - void PushID(const char* str_id_begin, const char* str_id_end) - { - ImGuiID id = GetID(str_id_begin, str_id_end); - gContext.mIDStack.push_back(id); - } - - void PushID(const void* ptr_id) - { - ImGuiID id = GetID(ptr_id); - gContext.mIDStack.push_back(id); - } - - void PushID(int int_id) - { - ImGuiID id = GetID(int_id); - gContext.mIDStack.push_back(id); - } - - void PopID() - { - IM_ASSERT(gContext.mIDStack.Size > 1); // Too many PopID(), or could be popping in a wrong/different window? - gContext.mIDStack.pop_back(); - } - - void AllowAxisFlip(bool value) - { - gContext.mAllowAxisFlip = value; - } - - void SetAxisLimit(float value) - { - gContext.mAxisLimit = value; - } - - void SetAxisMask(bool x, bool y, bool z) - { - gContext.mAxisMask = (x ? 1 : 0) + (y ? 2 : 0) + (z ? 4 : 0); - } - - void SetPlaneLimit(float value) - { - gContext.mPlaneLimit = value; - } - - bool IsOver(float* position, float pixelRadius) - { - const ImGuiIO& io = ImGui::GetIO(); - - float radius = - sqrtf((ImLengthSqr(worldToPos({ position[0], position[1], position[2], 0.0f }, gContext.mViewProjection) - io.MousePos))); - return radius < pixelRadius; - } - - bool Manipulate( - const float* view, - const float* projection, - OPERATION operation, - MODE mode, - float* matrix, - float* deltaMatrix, - const float* snap, - const float* localBounds, - const float* boundsSnap) - { - gContext.mDrawList->PushClipRect( - ImVec2(gContext.mX, gContext.mY), ImVec2(gContext.mX + gContext.mWidth, gContext.mY + gContext.mHeight), false); - - // Scale is always local or matrix will be skewed when applying world scale or oriented matrix - ComputeContext(view, projection, matrix, (operation & SCALE) ? LOCAL : mode); - - // set delta to identity - if (deltaMatrix) - { - ((matrix_t*)deltaMatrix)->SetToIdentity(); - } - - // behind camera - vec_t camSpacePosition; - camSpacePosition.TransformPoint(makeVect(0.f, 0.f, 0.f), gContext.mMVP); - if (!gContext.mIsOrthographic && camSpacePosition.z < 0.001f && !gContext.mbUsing) - { - return false; - } - - // -- - int type = MT_NONE; - bool manipulated = false; - if (gContext.mbEnable) - { - if (!gContext.mbUsingBounds) - { - manipulated = HandleTranslation(matrix, deltaMatrix, operation, type, snap) || - HandleScale(matrix, deltaMatrix, operation, type, snap) || HandleRotation(matrix, deltaMatrix, operation, type, snap); - } - } - - if (localBounds && !gContext.mbUsing) - { - HandleAndDrawLocalBounds(localBounds, (matrix_t*)matrix, boundsSnap, operation); - } - - gContext.mOperation = operation; - if (!gContext.mbUsingBounds) - { - DrawRotationGizmo(operation, type); - DrawTranslationGizmo(operation, type); - DrawScaleGizmo(operation, type); - DrawScaleUniveralGizmo(operation, type); - } - - gContext.mDrawList->PopClipRect(); - return manipulated; - } - - void SetGizmoSizeClipSpace(float value) - { - gContext.mGizmoSizeClipSpace = value; - } - - /////////////////////////////////////////////////////////////////////////////////////////////////// - void ComputeFrustumPlanes(vec_t* frustum, const float* clip) - { - frustum[0].x = clip[3] - clip[0]; - frustum[0].y = clip[7] - clip[4]; - frustum[0].z = clip[11] - clip[8]; - frustum[0].w = clip[15] - clip[12]; - - frustum[1].x = clip[3] + clip[0]; - frustum[1].y = clip[7] + clip[4]; - frustum[1].z = clip[11] + clip[8]; - frustum[1].w = clip[15] + clip[12]; - - frustum[2].x = clip[3] + clip[1]; - frustum[2].y = clip[7] + clip[5]; - frustum[2].z = clip[11] + clip[9]; - frustum[2].w = clip[15] + clip[13]; - - frustum[3].x = clip[3] - clip[1]; - frustum[3].y = clip[7] - clip[5]; - frustum[3].z = clip[11] - clip[9]; - frustum[3].w = clip[15] - clip[13]; - - frustum[4].x = clip[3] - clip[2]; - frustum[4].y = clip[7] - clip[6]; - frustum[4].z = clip[11] - clip[10]; - frustum[4].w = clip[15] - clip[14]; - - frustum[5].x = clip[3] + clip[2]; - frustum[5].y = clip[7] + clip[6]; - frustum[5].z = clip[11] + clip[10]; - frustum[5].w = clip[15] + clip[14]; - - for (int i = 0; i < 6; i++) - { - frustum[i].Normalize(); - } - } - - void DrawCubes(const float* view, const float* projection, const float* matrices, int matrixCount) - { - matrix_t viewInverse; - viewInverse.Inverse(*(matrix_t*)view); - - struct CubeFace - { - float z; - ImVec2 faceCoordsScreen[4]; - ImU32 color; - }; - CubeFace* faces = (CubeFace*)_malloca(sizeof(CubeFace) * matrixCount * 6); - - if (!faces) - { - return; - } - - vec_t frustum[6]; - matrix_t viewProjection = *(matrix_t*)view * *(matrix_t*)projection; - ComputeFrustumPlanes(frustum, viewProjection.m16); - - int cubeFaceCount = 0; - for (int cube = 0; cube < matrixCount; cube++) - { - const float* matrix = &matrices[cube * 16]; - - matrix_t res = *(matrix_t*)matrix * *(matrix_t*)view * *(matrix_t*)projection; - - for (int iFace = 0; iFace < 6; iFace++) - { - const int normalIndex = (iFace % 3); - const int perpXIndex = (normalIndex + 1) % 3; - const int perpYIndex = (normalIndex + 2) % 3; - const float invert = (iFace > 2) ? -1.f : 1.f; - - const vec_t faceCoords[4] = { - directionUnary[normalIndex] + directionUnary[perpXIndex] + directionUnary[perpYIndex], - directionUnary[normalIndex] + directionUnary[perpXIndex] - directionUnary[perpYIndex], - directionUnary[normalIndex] - directionUnary[perpXIndex] - directionUnary[perpYIndex], - directionUnary[normalIndex] - directionUnary[perpXIndex] + directionUnary[perpYIndex], - }; - - // clipping - /* - bool skipFace = false; - for (unsigned int iCoord = 0; iCoord < 4; iCoord++) - { - vec_t camSpacePosition; - camSpacePosition.TransformPoint(faceCoords[iCoord] * 0.5f * invert, res); - if (camSpacePosition.z < 0.001f) - { - skipFace = true; - break; - } - } - if (skipFace) - { - continue; - } - */ - vec_t centerPosition, centerPositionVP; - centerPosition.TransformPoint(directionUnary[normalIndex] * 0.5f * invert, *(matrix_t*)matrix); - centerPositionVP.TransformPoint(directionUnary[normalIndex] * 0.5f * invert, res); - - bool inFrustum = true; - for (int iFrustum = 0; iFrustum < 6; iFrustum++) - { - float dist = DistanceToPlane(centerPosition, frustum[iFrustum]); - if (dist < 0.f) - { - inFrustum = false; - break; - } - } - - if (!inFrustum) - { - continue; - } - CubeFace& cubeFace = faces[cubeFaceCount]; - - // 3D->2D - // ImVec2 faceCoordsScreen[4]; - for (unsigned int iCoord = 0; iCoord < 4; iCoord++) - { - cubeFace.faceCoordsScreen[iCoord] = worldToPos(faceCoords[iCoord] * 0.5f * invert, res); - } - - ImU32 directionColor = GetColorU32(DIRECTION_X + normalIndex); - cubeFace.color = directionColor | IM_COL32(0x80, 0x80, 0x80, 0); - - cubeFace.z = centerPositionVP.z / centerPositionVP.w; - cubeFaceCount++; - } - } - qsort( - faces, - cubeFaceCount, - sizeof(CubeFace), - [](void const* _a, void const* _b) - { - CubeFace* a = (CubeFace*)_a; - CubeFace* b = (CubeFace*)_b; - if (a->z < b->z) - { - return 1; - } - return -1; - }); - // draw face with lighter color - for (int iFace = 0; iFace < cubeFaceCount; iFace++) - { - const CubeFace& cubeFace = faces[iFace]; - gContext.mDrawList->AddConvexPolyFilled(cubeFace.faceCoordsScreen, 4, cubeFace.color); - } - - _freea(faces); - } - - void DrawGrid(const float* view, const float* projection, const float* matrix, const float gridSize) - { - matrix_t viewProjection = *(matrix_t*)view * *(matrix_t*)projection; - vec_t frustum[6]; - ComputeFrustumPlanes(frustum, viewProjection.m16); - matrix_t res = *(matrix_t*)matrix * viewProjection; - - for (float f = -gridSize; f <= gridSize; f += 1.f) - { - for (int dir = 0; dir < 2; dir++) - { - vec_t ptA = makeVect(dir ? -gridSize : f, 0.f, dir ? f : -gridSize); - vec_t ptB = makeVect(dir ? gridSize : f, 0.f, dir ? f : gridSize); - bool visible = true; - for (int i = 0; i < 6; i++) - { - float dA = DistanceToPlane(ptA, frustum[i]); - float dB = DistanceToPlane(ptB, frustum[i]); - if (dA < 0.f && dB < 0.f) - { - visible = false; - break; - } - if (dA > 0.f && dB > 0.f) - { - continue; - } - if (dA < 0.f) - { - float len = fabsf(dA - dB); - float t = fabsf(dA) / len; - ptA.Lerp(ptB, t); - } - if (dB < 0.f) - { - float len = fabsf(dB - dA); - float t = fabsf(dB) / len; - ptB.Lerp(ptA, t); - } - } - if (visible) - { - ImU32 col = IM_COL32(0x80, 0x80, 0x80, 0xFF); - col = (fmodf(fabsf(f), 10.f) < FLT_EPSILON) ? IM_COL32(0x90, 0x90, 0x90, 0xFF) : col; - col = (fabsf(f) < FLT_EPSILON) ? IM_COL32(0x40, 0x40, 0x40, 0xFF) : col; - - float thickness = 1.f; - thickness = (fmodf(fabsf(f), 10.f) < FLT_EPSILON) ? 1.5f : thickness; - thickness = (fabsf(f) < FLT_EPSILON) ? 2.3f : thickness; - - gContext.mDrawList->AddLine(worldToPos(ptA, res), worldToPos(ptB, res), col, thickness); - } - } - } - } - - void ViewManipulate( - float* view, - const float* projection, - OPERATION operation, - MODE mode, - float* matrix, - float length, - ImVec2 position, - ImVec2 size, - ImU32 backgroundColor) - { - // Scale is always local or matrix will be skewed when applying world scale or oriented matrix - ComputeContext(view, projection, matrix, (operation & SCALE) ? LOCAL : mode); - ViewManipulate(view, length, position, size, backgroundColor); - } - - void ViewManipulate(float* view, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor) - { - static bool isDraging = false; - static bool isClicking = false; - static vec_t interpolationUp; - static vec_t interpolationDir; - static int interpolationFrames = 0; - const vec_t referenceUp = makeVect(0.f, 1.f, 0.f); - - matrix_t svgView, svgProjection; - svgView = gContext.mViewMat; - svgProjection = gContext.mProjectionMat; - - ImGuiIO& io = ImGui::GetIO(); - gContext.mDrawList->AddRectFilled(position, position + size, backgroundColor); - matrix_t viewInverse; - viewInverse.Inverse(*(matrix_t*)view); - - const vec_t camTarget = viewInverse.v.position - viewInverse.v.dir * length; - - // view/projection matrices - const float distance = 3.f; - matrix_t cubeProjection, cubeView; - float fov = acosf(distance / (sqrtf(distance * distance + 3.f))) * RAD2DEG; - Perspective(fov / sqrtf(2.f), size.x / size.y, 0.01f, 1000.f, cubeProjection.m16); - - vec_t dir = makeVect(viewInverse.m[2][0], viewInverse.m[2][1], viewInverse.m[2][2]); - vec_t up = makeVect(viewInverse.m[1][0], viewInverse.m[1][1], viewInverse.m[1][2]); - vec_t eye = dir * distance; - vec_t zero = makeVect(0.f, 0.f); - LookAt(&eye.x, &zero.x, &up.x, cubeView.m16); - - // set context - gContext.mViewMat = cubeView; - gContext.mProjectionMat = cubeProjection; - ComputeCameraRay(gContext.mRayOrigin, gContext.mRayVector, position, size); - - const matrix_t res = cubeView * cubeProjection; - - // panels - static const ImVec2 panelPosition[9] = { ImVec2(0.75f, 0.75f), ImVec2(0.25f, 0.75f), ImVec2(0.f, 0.75f), - ImVec2(0.75f, 0.25f), ImVec2(0.25f, 0.25f), ImVec2(0.f, 0.25f), - ImVec2(0.75f, 0.f), ImVec2(0.25f, 0.f), ImVec2(0.f, 0.f) }; - - static const ImVec2 panelSize[9] = { ImVec2(0.25f, 0.25f), ImVec2(0.5f, 0.25f), ImVec2(0.25f, 0.25f), - ImVec2(0.25f, 0.5f), ImVec2(0.5f, 0.5f), ImVec2(0.25f, 0.5f), - ImVec2(0.25f, 0.25f), ImVec2(0.5f, 0.25f), ImVec2(0.25f, 0.25f) }; - - // tag faces - bool boxes[27]{}; - static int overBox = -1; - for (int iPass = 0; iPass < 2; iPass++) - { - for (int iFace = 0; iFace < 6; iFace++) - { - const int normalIndex = (iFace % 3); - const int perpXIndex = (normalIndex + 1) % 3; - const int perpYIndex = (normalIndex + 2) % 3; - const float invert = (iFace > 2) ? -1.f : 1.f; - const vec_t indexVectorX = directionUnary[perpXIndex] * invert; - const vec_t indexVectorY = directionUnary[perpYIndex] * invert; - const vec_t boxOrigin = directionUnary[normalIndex] * -invert - indexVectorX - indexVectorY; - - // plan local space - const vec_t n = directionUnary[normalIndex] * invert; - vec_t viewSpaceNormal = n; - vec_t viewSpacePoint = n * 0.5f; - viewSpaceNormal.TransformVector(cubeView); - viewSpaceNormal.Normalize(); - viewSpacePoint.TransformPoint(cubeView); - const vec_t viewSpaceFacePlan = BuildPlan(viewSpacePoint, viewSpaceNormal); - - // back face culling - if (viewSpaceFacePlan.w > 0.f) - { - continue; - } - - const vec_t facePlan = BuildPlan(n * 0.5f, n); - - const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, facePlan); - vec_t posOnPlan = gContext.mRayOrigin + gContext.mRayVector * len - (n * 0.5f); - - float localx = Dot(directionUnary[perpXIndex], posOnPlan) * invert + 0.5f; - float localy = Dot(directionUnary[perpYIndex], posOnPlan) * invert + 0.5f; - - // panels - const vec_t dx = directionUnary[perpXIndex]; - const vec_t dy = directionUnary[perpYIndex]; - const vec_t origin = directionUnary[normalIndex] - dx - dy; - for (int iPanel = 0; iPanel < 9; iPanel++) - { - vec_t boxCoord = - boxOrigin + indexVectorX * float(iPanel % 3) + indexVectorY * float(iPanel / 3) + makeVect(1.f, 1.f, 1.f); - const ImVec2 p = panelPosition[iPanel] * 2.f; - const ImVec2 s = panelSize[iPanel] * 2.f; - ImVec2 faceCoordsScreen[4]; - vec_t panelPos[4] = { - dx * p.x + dy * p.y, dx * p.x + dy * (p.y + s.y), dx * (p.x + s.x) + dy * (p.y + s.y), dx * (p.x + s.x) + dy * p.y - }; - - for (unsigned int iCoord = 0; iCoord < 4; iCoord++) - { - faceCoordsScreen[iCoord] = worldToPos((panelPos[iCoord] + origin) * 0.5f * invert, res, position, size); - } - - const ImVec2 panelCorners[2] = { panelPosition[iPanel], panelPosition[iPanel] + panelSize[iPanel] }; - bool insidePanel = localx > panelCorners[0].x && localx < panelCorners[1].x && localy > panelCorners[0].y && - localy < panelCorners[1].y; - int boxCoordInt = int(boxCoord.x * 9.f + boxCoord.y * 3.f + boxCoord.z); - IM_ASSERT(boxCoordInt < 27); - boxes[boxCoordInt] |= insidePanel && (!isDraging) && gContext.mbMouseOver; - - // draw face with lighter color - if (iPass) - { - ImU32 directionColor = GetColorU32(DIRECTION_X + normalIndex); - gContext.mDrawList->AddConvexPolyFilled( - faceCoordsScreen, - 4, - (directionColor | IM_COL32(0x80, 0x80, 0x80, 0x80)) | - (gContext.mIsViewManipulatorHovered ? IM_COL32(0x08, 0x08, 0x08, 0) : 0)); - if (boxes[boxCoordInt]) - { - gContext.mDrawList->AddConvexPolyFilled(faceCoordsScreen, 4, IM_COL32(0xF0, 0xA0, 0x60, 0x80)); - - if (io.MouseDown[0] && !isClicking && !isDraging && GImGui->ActiveId == 0) - { - overBox = boxCoordInt; - isClicking = true; - isDraging = true; - } - } - } - } - } - } - if (interpolationFrames) - { - interpolationFrames--; - vec_t newDir = viewInverse.v.dir; - newDir.Lerp(interpolationDir, 0.2f); - newDir.Normalize(); - vec_t newUp = viewInverse.v.up; - newUp.Lerp(interpolationUp, 0.3f); - newUp.Normalize(); - newUp = interpolationUp; - vec_t newEye = camTarget + newDir * length; - LookAt(&newEye.x, &camTarget.x, &newUp.x, view); - } - gContext.mIsViewManipulatorHovered = gContext.mbMouseOver && ImRect(position, position + size).Contains(io.MousePos); - - if (io.MouseDown[0] && (fabsf(io.MouseDelta[0]) || fabsf(io.MouseDelta[1])) && isClicking) - { - isClicking = false; - } - - if (!io.MouseDown[0]) - { - if (isClicking) - { - // apply new view direction - int cx = overBox / 9; - int cy = (overBox - cx * 9) / 3; - int cz = overBox % 3; - interpolationDir = makeVect(1.f - (float)cx, 1.f - (float)cy, 1.f - (float)cz); - interpolationDir.Normalize(); - - if (fabsf(Dot(interpolationDir, referenceUp)) > 1.0f - 0.01f) - { - vec_t right = viewInverse.v.right; - if (fabsf(right.x) > fabsf(right.z)) - { - right.z = 0.f; - } - else - { - right.x = 0.f; - } - right.Normalize(); - interpolationUp = Cross(interpolationDir, right); - interpolationUp.Normalize(); - } - else - { - interpolationUp = referenceUp; - } - interpolationFrames = 40; - } - isClicking = false; - isDraging = false; - } + if (isDraging) + { + matrix_t rx, ry, roll; - if (isDraging) - { - matrix_t rx, ry, roll; + rx.RotationAxis(referenceUp, -io.MouseDelta.x * 0.01f); + ry.RotationAxis(viewInverse.v.right, -io.MouseDelta.y * 0.01f); - rx.RotationAxis(referenceUp, -io.MouseDelta.x * 0.01f); - ry.RotationAxis(viewInverse.v.right, -io.MouseDelta.y * 0.01f); + roll = rx * ry; - roll = rx * ry; + vec_t newDir = viewInverse.v.dir; + newDir.TransformVector(roll); + newDir.Normalize(); - vec_t newDir = viewInverse.v.dir; - newDir.TransformVector(roll); + // clamp + vec_t planDir = Cross(viewInverse.v.right, referenceUp); + planDir.y = 0.f; + planDir.Normalize(); + float dt = Dot(planDir, newDir); + if (dt < 0.0f) + { + newDir += planDir * dt; newDir.Normalize(); + } - // clamp - vec_t planDir = Cross(viewInverse.v.right, referenceUp); - planDir.y = 0.f; - planDir.Normalize(); - float dt = Dot(planDir, newDir); - if (dt < 0.0f) - { - newDir += planDir * dt; - newDir.Normalize(); - } - - vec_t newEye = camTarget + newDir * length; - LookAt(&newEye.x, &camTarget.x, &referenceUp.x, view); - } + vec_t newEye = camTarget + newDir * length; + LookAt(&newEye.x, &camTarget.x, &referenceUp.x, view); + } - gContext.mbUsingViewManipulate = (interpolationFrames != 0) || isDraging; + gContext.mbUsingViewManipulate = (interpolationFrames != 0) || isDraging; - // restore view/projection because it was used to compute ray - ComputeContext(svgView.m16, svgProjection.m16, gContext.mModelSource.m16, gContext.mMode); - } -}; // namespace IMGUIZMO_NAMESPACE + // restore view/projection because it was used to compute ray + ComputeContext(svgView.m16, svgProjection.m16, gContext.mModelSource.m16, gContext.mMode); + } +}; diff --git a/Gems/ImGuizmo/Code/3rdParty/ImGuizmo/ImGuizmo.h b/Gems/ImGuizmo/Code/3rdParty/ImGuizmo/ImGuizmo.h index 98095d27..56cc6dcc 100644 --- a/Gems/ImGuizmo/Code/3rdParty/ImGuizmo/ImGuizmo.h +++ b/Gems/ImGuizmo/Code/3rdParty/ImGuizmo/ImGuizmo.h @@ -27,8 +27,8 @@ // History : // 2025/01/10 Adjustments in library during integration into O3DE // 2019/11/03 View gizmo -// 2016/09/11 Behind camera culling. Scaling Delta matrix not multiplied by source matrix scales. local/world rotation and translation -// fixed. Display message is incorrect (X: ... Y:...) in local mode. 2016/09/09 Hatched negative axis. Snapping. Documentation update. +// 2016/09/11 Behind camera culling. Scaling Delta matrix not multiplied by source matrix scales. local/world rotation and translation fixed. Display message is incorrect (X: ... Y:...) in local mode. +// 2016/09/09 Hatched negative axis. Snapping. Documentation update. // 2016/09/04 Axis switch and translation plan autohiding. Scale transform stability improved // 2016/09/01 Mogwai changed to Manipulate. Draw debug cube. Fixed inverted scale. Mixing scale and translation/rotation gives bad results. // 2016/08/31 First version @@ -121,174 +121,154 @@ struct ImGuiWindow; namespace IMGUIZMO_NAMESPACE { - // call inside your own window and before Manipulate() in order to draw gizmo to that window. - // Or pass a specific ImDrawList to draw to (e.g. ImGui::GetForegroundDrawList()). - IMGUI_API void SetDrawlist(ImDrawList* drawlist = nullptr); + // call inside your own window and before Manipulate() in order to draw gizmo to that window. + // Or pass a specific ImDrawList to draw to (e.g. ImGui::GetForegroundDrawList()). + IMGUI_API void SetDrawlist(ImDrawList* drawlist = nullptr); - // call BeginFrame right after ImGui_XXXX_NewFrame(); - IMGUI_API void BeginFrame(); + // call BeginFrame right after ImGui_XXXX_NewFrame(); + IMGUI_API void BeginFrame(); - // this is necessary because when imguizmo is compiled into a dll, and imgui into another - // globals are not shared between them. - // More details at - // https://stackoverflow.com/questions/19373061/what-happens-to-global-and-static-variables-in-a-shared-library-when-it-is-dynam expose - // method to set imgui context - IMGUI_API void SetImGuiContext(ImGuiContext* ctx); + // this is necessary because when imguizmo is compiled into a dll, and imgui into another + // globals are not shared between them. + // More details at https://stackoverflow.com/questions/19373061/what-happens-to-global-and-static-variables-in-a-shared-library-when-it-is-dynam + // expose method to set imgui context + IMGUI_API void SetImGuiContext(ImGuiContext* ctx); - // return true if mouse cursor is over any gizmo control (axis, plan or screen component) - IMGUI_API bool IsOver(); + // return true if mouse cursor is over any gizmo control (axis, plan or screen component) + IMGUI_API bool IsOver(); - // return true if mouse IsOver or if the gizmo is in moving state - IMGUI_API bool IsUsing(); + // return true if mouse IsOver or if the gizmo is in moving state + IMGUI_API bool IsUsing(); - // return true if the view gizmo is in moving state - IMGUI_API bool IsUsingViewManipulate(); - // only check if your mouse is over the view manipulator - no matter whether it's active or not - IMGUI_API bool IsViewManipulateHovered(); + // return true if the view gizmo is in moving state + IMGUI_API bool IsUsingViewManipulate(); + // only check if your mouse is over the view manipulator - no matter whether it's active or not + IMGUI_API bool IsViewManipulateHovered(); - // return true if any gizmo is in moving state - IMGUI_API bool IsUsingAny(); + // return true if any gizmo is in moving state + IMGUI_API bool IsUsingAny(); - // enable/disable the gizmo. Stay in the state until next call to Enable. - // gizmo is rendered with gray half transparent color when disabled - IMGUI_API void Enable(bool enable); + // enable/disable the gizmo. Stay in the state until next call to Enable. + // gizmo is rendered with gray half transparent color when disabled + IMGUI_API void Enable(bool enable); - // helper functions for manualy editing translation/rotation/scale with an input float - // translation, rotation and scale float points to 3 floats each - // Angles are in degrees (more suitable for human editing) - // example: - // float matrixTranslation[3], matrixRotation[3], matrixScale[3]; - // ImGuizmo::DecomposeMatrixToComponents(gizmoMatrix.m16, matrixTranslation, matrixRotation, matrixScale); - // ImGui::InputFloat3("Tr", matrixTranslation, 3); - // ImGui::InputFloat3("Rt", matrixRotation, 3); - // ImGui::InputFloat3("Sc", matrixScale, 3); - // ImGuizmo::RecomposeMatrixFromComponents(matrixTranslation, matrixRotation, matrixScale, gizmoMatrix.m16); - // - // These functions have some numerical stability issues for now. Use with caution. - IMGUI_API void DecomposeMatrixToComponents(const float* matrix, float* translation, float* rotation, float* scale); - IMGUI_API void RecomposeMatrixFromComponents(const float* translation, const float* rotation, const float* scale, float* matrix); + // helper functions for manualy editing translation/rotation/scale with an input float + // translation, rotation and scale float points to 3 floats each + // Angles are in degrees (more suitable for human editing) + // example: + // float matrixTranslation[3], matrixRotation[3], matrixScale[3]; + // ImGuizmo::DecomposeMatrixToComponents(gizmoMatrix.m16, matrixTranslation, matrixRotation, matrixScale); + // ImGui::InputFloat3("Tr", matrixTranslation, 3); + // ImGui::InputFloat3("Rt", matrixRotation, 3); + // ImGui::InputFloat3("Sc", matrixScale, 3); + // ImGuizmo::RecomposeMatrixFromComponents(matrixTranslation, matrixRotation, matrixScale, gizmoMatrix.m16); + // + // These functions have some numerical stability issues for now. Use with caution. + IMGUI_API void DecomposeMatrixToComponents(const float* matrix, float* translation, float* rotation, float* scale); + IMGUI_API void RecomposeMatrixFromComponents(const float* translation, const float* rotation, const float* scale, float* matrix); - IMGUI_API void SetRect(float x, float y, float width, float height); - // default is false - IMGUI_API void SetOrthographic(bool isOrthographic); + IMGUI_API void SetRect(float x, float y, float width, float height); + // default is false + IMGUI_API void SetOrthographic(bool isOrthographic); - // Render a cube with face color corresponding to face normal. Usefull for debug/tests - IMGUI_API void DrawCubes(const float* view, const float* projection, const float* matrices, int matrixCount); - IMGUI_API void DrawGrid(const float* view, const float* projection, const float* matrix, const float gridSize); + // Render a cube with face color corresponding to face normal. Usefull for debug/tests + IMGUI_API void DrawCubes(const float* view, const float* projection, const float* matrices, int matrixCount); + IMGUI_API void DrawGrid(const float* view, const float* projection, const float* matrix, const float gridSize); - // call it when you want a gizmo - // Needs view and projection matrices. - // matrix parameter is the source matrix (where will be gizmo be drawn) and might be transformed by the function. Return deltaMatrix is - // optional translation is applied in world space + // call it when you want a gizmo + // Needs view and projection matrices. + // matrix parameter is the source matrix (where will be gizmo be drawn) and might be transformed by the function. Return deltaMatrix is optional + // translation is applied in world space - IMGUI_API bool Manipulate( - const float* view, - const float* projection, - OPERATION operation, - MODE mode, - float* matrix, - float* deltaMatrix = NULL, - const float* snap = NULL, - const float* localBounds = NULL, - const float* boundsSnap = NULL); - // - // Please note that this cubeview is patented by Autodesk : https://patents.google.com/patent/US7782319B2/en - // It seems to be a defensive patent in the US. I don't think it will bring troubles using it as - // other software are using the same mechanics. But just in case, you are now warned! - // - IMGUI_API void ViewManipulate(float* view, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor); + IMGUI_API bool Manipulate(const float* view, const float* projection, OPERATION operation, MODE mode, float* matrix, float* deltaMatrix = NULL, const float* snap = NULL, const float* localBounds = NULL, const float* boundsSnap = NULL); + // + // Please note that this cubeview is patented by Autodesk : https://patents.google.com/patent/US7782319B2/en + // It seems to be a defensive patent in the US. I don't think it will bring troubles using it as + // other software are using the same mechanics. But just in case, you are now warned! + // + IMGUI_API void ViewManipulate(float* view, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor); - // use this version if you did not call Manipulate before and you are just using ViewManipulate - IMGUI_API void ViewManipulate( - float* view, - const float* projection, - OPERATION operation, - MODE mode, - float* matrix, - float length, - ImVec2 position, - ImVec2 size, - ImU32 backgroundColor); + // use this version if you did not call Manipulate before and you are just using ViewManipulate + IMGUI_API void ViewManipulate(float* view, const float* projection, OPERATION operation, MODE mode, float* matrix, float length, ImVec2 position, ImVec2 size, ImU32 backgroundColor); - IMGUI_API void SetAlternativeWindow(ImGuiWindow* window); + IMGUI_API void SetAlternativeWindow(ImGuiWindow* window); - [[deprecated("Use PushID/PopID instead.")]] IMGUI_API void SetID(int id); + [[deprecated("Use PushID/PopID instead.")]] + IMGUI_API void SetID(int id); - // ID stack/scopes - // Read the FAQ (docs/FAQ.md or http://dearimgui.org/faq) for more details about how ID are handled in dear imgui. - // - Those questions are answered and impacted by understanding of the ID stack system: - // - "Q: Why is my widget not reacting when I click on it?" - // - "Q: How can I have widgets with an empty label?" - // - "Q: How can I have multiple widgets with the same label?" - // - Short version: ID are hashes of the entire ID stack. If you are creating widgets in a loop you most likely - // want to push a unique identifier (e.g. object pointer, loop index) to uniquely differentiate them. - // - You can also use the "Label##foobar" syntax within widget label to distinguish them from each others. - // - In this header file we use the "label"/"name" terminology to denote a string that will be displayed + used as an ID, - // whereas "str_id" denote a string that is only used as an ID and not normally displayed. - IMGUI_API void PushID(const char* str_id); // push string into the ID stack (will hash string). - IMGUI_API void PushID(const char* str_id_begin, const char* str_id_end); // push string into the ID stack (will hash string). - IMGUI_API void PushID(const void* ptr_id); // push pointer into the ID stack (will hash pointer). - IMGUI_API void PushID(int int_id); // push integer into the ID stack (will hash integer). - IMGUI_API void PopID(); // pop from the ID stack. - IMGUI_API ImGuiID GetID(const char* str_id); // calculate unique ID (hash of whole ID stack + given parameter). e.g. if you want to - // query into ImGuiStorage yourself - IMGUI_API ImGuiID GetID(const char* str_id_begin, const char* str_id_end); - IMGUI_API ImGuiID GetID(const void* ptr_id); + // ID stack/scopes + // Read the FAQ (docs/FAQ.md or http://dearimgui.org/faq) for more details about how ID are handled in dear imgui. + // - Those questions are answered and impacted by understanding of the ID stack system: + // - "Q: Why is my widget not reacting when I click on it?" + // - "Q: How can I have widgets with an empty label?" + // - "Q: How can I have multiple widgets with the same label?" + // - Short version: ID are hashes of the entire ID stack. If you are creating widgets in a loop you most likely + // want to push a unique identifier (e.g. object pointer, loop index) to uniquely differentiate them. + // - You can also use the "Label##foobar" syntax within widget label to distinguish them from each others. + // - In this header file we use the "label"/"name" terminology to denote a string that will be displayed + used as an ID, + // whereas "str_id" denote a string that is only used as an ID and not normally displayed. + IMGUI_API void PushID(const char* str_id); // push string into the ID stack (will hash string). + IMGUI_API void PushID(const char* str_id_begin, const char* str_id_end); // push string into the ID stack (will hash string). + IMGUI_API void PushID(const void* ptr_id); // push pointer into the ID stack (will hash pointer). + IMGUI_API void PushID(int int_id); // push integer into the ID stack (will hash integer). + IMGUI_API void PopID(); // pop from the ID stack. + IMGUI_API ImGuiID GetID(const char* str_id); // calculate unique ID (hash of whole ID stack + given parameter). e.g. if you want to query into ImGuiStorage yourself + IMGUI_API ImGuiID GetID(const char* str_id_begin, const char* str_id_end); + IMGUI_API ImGuiID GetID(const void* ptr_id); - // return true if the cursor is over the operation's gizmo - IMGUI_API bool IsOver(OPERATION op); - IMGUI_API void SetGizmoSizeClipSpace(float value); + // return true if the cursor is over the operation's gizmo + IMGUI_API bool IsOver(OPERATION op); + IMGUI_API void SetGizmoSizeClipSpace(float value); - // Allow axis to flip - // When true (default), the guizmo axis flip for better visibility - // When false, they always stay along the positive world/local axis - IMGUI_API void AllowAxisFlip(bool value); + // Allow axis to flip + // When true (default), the guizmo axis flip for better visibility + // When false, they always stay along the positive world/local axis + IMGUI_API void AllowAxisFlip(bool value); - // Configure the limit where axis are hidden - IMGUI_API void SetAxisLimit(float value); - // Set an axis mask to permanently hide a given axis (true -> hidden, false -> shown) - IMGUI_API void SetAxisMask(bool x, bool y, bool z); - // Configure the limit where planes are hiden - IMGUI_API void SetPlaneLimit(float value); - // from a x,y,z point in space and using Manipulation view/projection matrix, check if mouse is in pixel radius distance of that - // projected point - IMGUI_API bool IsOver(float* position, float pixelRadius); + // Configure the limit where axis are hidden + IMGUI_API void SetAxisLimit(float value); + // Set an axis mask to permanently hide a given axis (true -> hidden, false -> shown) + IMGUI_API void SetAxisMask(bool x, bool y, bool z); + // Configure the limit where planes are hiden + IMGUI_API void SetPlaneLimit(float value); + // from a x,y,z point in space and using Manipulation view/projection matrix, check if mouse is in pixel radius distance of that projected point + IMGUI_API bool IsOver(float* position, float pixelRadius); - enum COLOR - { - DIRECTION_X, // directionColor[0] - DIRECTION_Y, // directionColor[1] - DIRECTION_Z, // directionColor[2] - PLANE_X, // planeColor[0] - PLANE_Y, // planeColor[1] - PLANE_Z, // planeColor[2] - SELECTION, // selectionColor - INACTIVE, // inactiveColor - TRANSLATION_LINE, // translationLineColor - SCALE_LINE, - ROTATION_USING_BORDER, - ROTATION_USING_FILL, - HATCHED_AXIS_LINES, - TEXT, - TEXT_SHADOW, - COUNT - }; + enum COLOR + { + DIRECTION_X, // directionColor[0] + DIRECTION_Y, // directionColor[1] + DIRECTION_Z, // directionColor[2] + PLANE_X, // planeColor[0] + PLANE_Y, // planeColor[1] + PLANE_Z, // planeColor[2] + SELECTION, // selectionColor + INACTIVE, // inactiveColor + TRANSLATION_LINE, // translationLineColor + SCALE_LINE, + ROTATION_USING_BORDER, + ROTATION_USING_FILL, + HATCHED_AXIS_LINES, + TEXT, + TEXT_SHADOW, + COUNT + }; - struct Style - { - IMGUI_API Style(); + struct Style + { + IMGUI_API Style(); - float TranslationLineThickness; // Thickness of lines for translation gizmo - float TranslationLineArrowSize; // Size of arrow at the end of lines for translation gizmo - float RotationLineThickness; // Thickness of lines for rotation gizmo - float RotationOuterLineThickness; // Thickness of line surrounding the rotation gizmo - float ScaleLineThickness; // Thickness of lines for scale gizmo - float ScaleLineCircleSize; // Size of circle at the end of lines for scale gizmo - float HatchedAxisLineThickness; // Thickness of hatched axis lines - float CenterCircleSize; // Size of circle at the center of the translate/scale gizmo + float TranslationLineThickness; // Thickness of lines for translation gizmo + float TranslationLineArrowSize; // Size of arrow at the end of lines for translation gizmo + float RotationLineThickness; // Thickness of lines for rotation gizmo + float RotationOuterLineThickness; // Thickness of line surrounding the rotation gizmo + float ScaleLineThickness; // Thickness of lines for scale gizmo + float ScaleLineCircleSize; // Size of circle at the end of lines for scale gizmo + float HatchedAxisLineThickness; // Thickness of hatched axis lines + float CenterCircleSize; // Size of circle at the center of the translate/scale gizmo - ImVec4 Colors[COLOR::COUNT]; - }; + ImVec4 Colors[COLOR::COUNT]; + }; - IMGUI_API Style& GetStyle(); -} // namespace IMGUIZMO_NAMESPACE + IMGUI_API Style& GetStyle(); +} From 12dc52263d7b53a3e35832c25eb090d837259932 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 09:50:52 +0100 Subject: [PATCH 086/175] use max buffer line size for memory reserve Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 4a7b4b5c..6d97878b 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -75,7 +75,7 @@ namespace FPSProfiler } // Reserve log entries buffer size based on known auto save per frame - m_configuration.m_AutoSave ? m_logBuffer.reserve(160 * m_configuration.m_AutoSaveAtFrame * 2) + m_configuration.m_AutoSave ? m_logBuffer.reserve(MAX_LOG_BUFFER_LINE_SIZE * m_configuration.m_AutoSaveAtFrame * 2) : m_logBuffer.reserve(MAX_LOG_BUFFER_SIZE); } From 6c58aa39beaea53d829da10475e1652e27b26f60 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 10:07:42 +0100 Subject: [PATCH 087/175] remove if else | improve readability Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 6d97878b..0c065eed 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -115,9 +115,10 @@ namespace FPSProfiler char logEntry[MAX_LOG_BUFFER_LINE_SIZE]; int logEntryLength = 0; - // Initialize memory usage values + // Initialize statistics data float usedCpu = -1.0f, reservedCpu = -1.0f; float usedGpu = -1.0f, reservedGpu = -1.0f; + const char* logEntryFormat = "-1,-1.0,-1.0,-1.0,-1.0,-1.0,%.2f,%.2f,%.2f,%.2f\n"; if (m_configuration.m_SaveCpuData) { @@ -133,35 +134,26 @@ namespace FPSProfiler reservedGpu = BytesToMB(gpuReserved); } - // Format log entry - if (m_configuration.m_SaveFpsData) - { - logEntryLength = azsnprintf( - logEntry, - MAX_LOG_BUFFER_LINE_SIZE, - "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n", - m_frameCount, - deltaTime, - m_currentFps, - m_minFps, - m_maxFps, - m_avgFps, - usedCpu, - reservedCpu, - usedGpu, - reservedGpu); - } - else + if (m_configuration.m_SaveCpuData) { - logEntryLength = azsnprintf( - logEntry, - MAX_LOG_BUFFER_LINE_SIZE, - "-1,-1.0,-1.0,-1.0,-1.0,-1.0,%.2f,%.2f,%.2f,%.2f\n", - usedCpu, - reservedCpu, - usedGpu, - reservedGpu); + logEntryFormat = "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n"; } + + // Add log entry + logEntryLength = azsnprintf( + logEntry, + MAX_LOG_BUFFER_LINE_SIZE, + logEntryFormat, + m_frameCount, + deltaTime, + m_currentFps, + m_minFps, + m_maxFps, + m_avgFps, + usedCpu, + reservedCpu, + usedGpu, + reservedGpu); m_logBuffer.insert(m_logBuffer.end(), logEntry, logEntry + logEntryLength); // Auto save From 1832ce7fceb2f067e5a99fe0e9663d5cc6bd108e Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 10:09:32 +0100 Subject: [PATCH 088/175] remove profile flag Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 0c065eed..502f47b3 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -91,8 +91,6 @@ namespace FPSProfiler void FPSProfilerSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { - AZ_PROFILE_SCOPE(AzCore, "FPSProfiler::OnTick()"); - if (!m_isProfiling) { return; From 87248527aa3b16b4f7cf63bb4f1e241d7ca475b1 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 10:34:44 +0100 Subject: [PATCH 089/175] add warning print for stop/reset Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 502f47b3..00ca8770 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -170,6 +170,7 @@ namespace FPSProfiler { if (m_isProfiling) { + AZ_Warning("FPS Profiler", false, "Profiler already activated."); return; } @@ -177,7 +178,8 @@ namespace FPSProfiler ResetProfilingData(); CreateLogFile(); - if (!AZ::TickBus::Handler::BusIsConnected()) // Connect TickBus only if not already connected + // Connect TickBus only if not already connected + if (!AZ::TickBus::Handler::BusIsConnected()) { AZ::TickBus::Handler::BusConnect(); } @@ -191,12 +193,14 @@ namespace FPSProfiler { if (!m_isProfiling) { + AZ_Warning("FPS Profiler", false, "Profiler already stopped."); return; } m_isProfiling = false; - if (AZ::TickBus::Handler::BusIsConnected()) // Only disconnect if actually connected + // Disconnect TickBus only if actually connected + if (AZ::TickBus::Handler::BusIsConnected()) { AZ::TickBus::Handler::BusDisconnect(); } @@ -220,7 +224,7 @@ namespace FPSProfiler // Notify - Profile Reset FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileReset, m_configuration); - AZ_Printf("FPS Profiler", "Profiling reset."); + AZ_Printf("FPS Profiler", "Profiling data reseted."); } bool FPSProfilerSystemComponent::IsProfiling() const From 4262502faf78b7b985abe230e4d0469cbf8973ce Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 13:14:17 +0100 Subject: [PATCH 090/175] queue start profile on activation | add aditional bool flag Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 30 +++++++++++++------ .../Clients/FPSProfilerSystemComponent.h | 1 + 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 00ca8770..333db479 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -16,7 +16,7 @@ namespace FPSProfiler serializeContext->Class() ->Version(0) ->Field("m_Configuration", &FPSProfilerSystemComponent::m_configuration) - ->Field("m_profileOnGameStart", &FPSProfilerSystemComponent::m_isProfiling); + ->Field("m_profileOnGameStart", &FPSProfilerSystemComponent::m_profileOnGameStart); } } @@ -40,7 +40,7 @@ namespace FPSProfiler FPSProfilerSystemComponent::FPSProfilerSystemComponent(const FPSProfilerConfig& config, bool profileOnGameStart) : m_configuration(AZStd::move(config)) - , m_isProfiling(profileOnGameStart) + , m_profileOnGameStart(profileOnGameStart) { if (FPSProfilerInterface::Get() == nullptr) { @@ -65,18 +65,22 @@ namespace FPSProfiler } FPSProfilerRequestBus::Handler::BusConnect(); // connect first to broadcast notifications - ResetProfilingData(); AZ::TickBus::Handler::BusConnect(); // connect last, after setup - AZ_Printf("FPS Profiler", "Activating FPSProfiler"); - - if (IsAnySaveOptionEnabled()) - { - CreateLogFile(); - } // Reserve log entries buffer size based on known auto save per frame m_configuration.m_AutoSave ? m_logBuffer.reserve(MAX_LOG_BUFFER_LINE_SIZE * m_configuration.m_AutoSaveAtFrame * 2) : m_logBuffer.reserve(MAX_LOG_BUFFER_SIZE); + + if (m_profileOnGameStart) + { + AZ::TickBus::QueueFunction( + [this]() + { + StartProfiling(); + }); + } + + AZ_Printf("FPS Profiler", "FPSProfiler activated."); } void FPSProfilerSystemComponent::Deactivate() @@ -91,6 +95,8 @@ namespace FPSProfiler void FPSProfilerSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { + AZ_PROFILE_SCOPE(AzCore, "FPSProfiler"); + if (!m_isProfiling) { return; @@ -351,6 +357,12 @@ namespace FPSProfiler void FPSProfilerSystemComponent::CreateLogFile() { + if (!IsAnySaveOptionEnabled()) + { + AZ_Warning("FPSProfiler", false, "None save option selected. Skipping file creation."); + return; + } + if (!IsPathValid(m_configuration.m_OutputFilename)) { m_configuration.m_OutputFilename = "@user@/fps_log.csv"; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 749961d0..191a18a0 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -59,6 +59,7 @@ namespace FPSProfiler // Profiling State bool m_isProfiling = false; //!< Flag to indicate if profiling is active + bool m_profileOnGameStart = false; //!< Should start profiling at game start. // FPS Tracking Data float m_minFps = 0.0f; //!< Lowest FPS value recorded From 9912fcaabbf9911c097f3568b6e51518e0d01188 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 13:21:54 +0100 Subject: [PATCH 091/175] explanation why use minFps = FloatMax Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 333db479..e4a34899 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -219,7 +219,7 @@ namespace FPSProfiler void FPSProfilerSystemComponent::ResetProfilingData() { - m_minFps = AZ::Constants::FloatMax; + m_minFps = AZ::Constants::FloatMax; // Start at max to ensure the first valid FPS sets the minimum correctly in AZStd::min m_maxFps = 0.0f; m_avgFps = 0.0f; m_currentFps = 0.0f; From 01992e5212ab1fb568a0b266f1fc5e4c8c0d801a Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 13:27:30 +0100 Subject: [PATCH 092/175] clean up | remove profiling flag Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index e4a34899..6863f0b0 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -95,8 +95,6 @@ namespace FPSProfiler void FPSProfilerSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { - AZ_PROFILE_SCOPE(AzCore, "FPSProfiler"); - if (!m_isProfiling) { return; From 702baf7c0dafe37199536e275adf61af54333c3b Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 14:20:33 +0100 Subject: [PATCH 093/175] refactor layout of config Signed-off-by: Wojciech Czerski --- .../Code/Source/Tools/FPSProfilerConfig.cpp | 30 +++++++++---------- .../Code/Source/Tools/FPSProfilerConfig.h | 2 +- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index 083f108d..348b4741 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -14,10 +14,10 @@ namespace FPSProfiler ->Field("m_AutoSave", &FPSProfilerConfig::m_AutoSave) ->Field("m_AutoSaveAtFrame", &FPSProfilerConfig::m_AutoSaveAtFrame) ->Field("m_SaveWithTimestamp", &FPSProfilerConfig::m_SaveWithTimestamp) - ->Field("m_NearZeroPrecision", &FPSProfilerConfig::m_NearZeroPrecision) ->Field("m_SaveFPSData", &FPSProfilerConfig::m_SaveFpsData) ->Field("m_SaveCPUData", &FPSProfilerConfig::m_SaveCpuData) ->Field("m_SaveGPUData", &FPSProfilerConfig::m_SaveGpuData) + ->Field("m_NearZeroPrecision", &FPSProfilerConfig::m_NearZeroPrecision) ->Field("m_ShowFPS", &FPSProfilerConfig::m_ShowFps); if (AZ::EditContext* editContext = serializeContext->GetEditContext()) @@ -29,6 +29,7 @@ namespace FPSProfiler ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ->ClassElement(AZ::Edit::ClassElements::Group, "File Settings") ->DataElement( AZ::Edit::UIHandlers::Default, &FPSProfilerConfig::m_OutputFilename, @@ -40,7 +41,7 @@ namespace FPSProfiler AZ::Edit::UIHandlers::Default, &FPSProfilerConfig::m_AutoSave, "Auto Save", - "When enabled, system will auto save after specified frame occurrance.") + "When enabled, system will auto save after specified frame occurrence.") ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree) ->DataElement( @@ -63,19 +64,7 @@ namespace FPSProfiler "Timestamp", "When enabled, system will save files with timestamp postfix of current date and hour.") - ->ClassElement(AZ::Edit::ClassElements::Group, "Precision Settings") - - ->DataElement( - AZ::Edit::UIHandlers::Default, - &FPSProfilerConfig::m_NearZeroPrecision, - "Near Zero Precision", - "Specify near Zero precision, that will be used for system.") - ->Attribute(AZ::Edit::Attributes::Min, 0.0f) - ->Attribute(AZ::Edit::Attributes::Max, 0.1f) - ->Attribute(AZ::Edit::Attributes::Step, 0.00001f) - - ->ClassElement(AZ::Edit::ClassElements::Group, "Data Settings") - + ->ClassElement(AZ::Edit::ClassElements::Group, "Statistics Settings") ->DataElement( AZ::Edit::UIHandlers::Default, &FPSProfilerConfig::m_SaveFpsData, @@ -94,8 +83,17 @@ namespace FPSProfiler "Save CPU Data", "When enabled, system will collect CPU usage data into csv.") - ->ClassElement(AZ::Edit::ClassElements::Group, "Debug Settings") + ->ClassElement(AZ::Edit::ClassElements::Group, "Precision Settings") + ->DataElement( + AZ::Edit::UIHandlers::Default, + &FPSProfilerConfig::m_NearZeroPrecision, + "Near Zero Precision", + "Specify near Zero precision, that will be used for system.") + ->Attribute(AZ::Edit::Attributes::Min, 0.0f) + ->Attribute(AZ::Edit::Attributes::Max, 0.1f) + ->Attribute(AZ::Edit::Attributes::Step, 0.00001f) + ->ClassElement(AZ::Edit::ClassElements::Group, "Debug Settings") ->DataElement( AZ::Edit::UIHandlers::Default, &FPSProfilerConfig::m_ShowFps, diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h index 1c3b4634..42df9f2a 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h @@ -16,10 +16,10 @@ namespace FPSProfiler bool m_AutoSave = true; int m_AutoSaveAtFrame = 100; bool m_SaveWithTimestamp = true; - float m_NearZeroPrecision = 0.01f; bool m_SaveFpsData = true; bool m_SaveGpuData = true; bool m_SaveCpuData = true; + float m_NearZeroPrecision = 0.01f; bool m_ShowFps = true; }; } // namespace FPSProfiler From 57911e9bf55a94e5e10d646f13263c1839e5060a Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 15:12:33 +0100 Subject: [PATCH 094/175] rename config file Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 6 +-- .../Include/FPSProfiler/FPSProfilerTypeIds.h | 2 +- .../Clients/FPSProfilerSystemComponent.cpp | 2 +- .../Clients/FPSProfilerSystemComponent.h | 4 +- .../Code/Source/Tools/FPSProfilerConfig.cpp | 45 ++++++++++--------- .../Code/Source/Tools/FPSProfilerConfig.h | 32 ++++++++++++- .../FPSProfilerEditorSystemComponent.cpp | 2 +- .../Tools/FPSProfilerEditorSystemComponent.h | 2 +- 8 files changed, 62 insertions(+), 33 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index bb863fd3..d43bb4d0 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -164,7 +164,7 @@ namespace FPSProfiler * @brief Called when the profiling process starts. * @param config The configuration settings used for the profiling session. */ - virtual void OnProfileStart(const FPSProfilerConfig& config) + virtual void OnProfileStart(const FPSProfilerConfigFile& config) { } @@ -172,7 +172,7 @@ namespace FPSProfiler * @brief Called when the profiling data is reset. * @param config The configuration settings used for the profiling session. */ - virtual void OnProfileReset(const FPSProfilerConfig& config) + virtual void OnProfileReset(const FPSProfilerConfigFile& config) { } @@ -180,7 +180,7 @@ namespace FPSProfiler * @brief Called when the profiling process stops. * @param config The configuration settings used for the profiling session. */ - virtual void OnProfileStop(const FPSProfilerConfig& config) + virtual void OnProfileStop(const FPSProfilerConfigFile& config) { } }; diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h index 78d83c4c..3a3e6295 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h @@ -6,7 +6,7 @@ namespace FPSProfiler // System Component TypeIds inline constexpr const char* FPSProfilerSystemComponentTypeId = "{B11CE88E-E1C5-404F-83D2-0D3850445A13}"; inline constexpr const char* FPSProfilerEditorSystemComponentTypeId = "{F4308920-CD0B-4A2E-91DE-2EC1E970F97A}"; - inline constexpr const char* FPSProfilerDataTypeId = "{70857242-4363-403C-ACF1-4A401B1024B5}"; + inline constexpr const char* FPSProfilerConfigFileTypeId = "{70857242-4363-403C-ACF1-4A401B1024B5}"; // Module derived classes TypeIds inline constexpr const char* FPSProfilerModuleInterfaceTypeId = "{77EF155C-6E75-41B1-A939-AF5E2FE4FC6B}"; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 6863f0b0..9705aadc 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -38,7 +38,7 @@ namespace FPSProfiler } } - FPSProfilerSystemComponent::FPSProfilerSystemComponent(const FPSProfilerConfig& config, bool profileOnGameStart) + FPSProfilerSystemComponent::FPSProfilerSystemComponent(const FPSProfilerConfigFile& config, bool profileOnGameStart) : m_configuration(AZStd::move(config)) , m_profileOnGameStart(profileOnGameStart) { diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 191a18a0..b19d445d 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -23,7 +23,7 @@ namespace FPSProfiler static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); FPSProfilerSystemComponent(); - explicit FPSProfilerSystemComponent(const FPSProfilerConfig& config, bool profileOnGameStart); + explicit FPSProfilerSystemComponent(const FPSProfilerConfigFile& config, bool profileOnGameStart); ~FPSProfilerSystemComponent() override; protected: @@ -55,7 +55,7 @@ namespace FPSProfiler private: // Profiler Configuration - FPSProfilerConfig m_configuration; //!< Stores editor settings for the profiler + FPSProfilerConfigFile m_configuration; //!< Stores editor settings for the profiler // Profiling State bool m_isProfiling = false; //!< Flag to indicate if profiling is active diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index 348b4741..453ade3b 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -4,26 +4,27 @@ namespace FPSProfiler { - void FPSProfilerConfig::Reflect(AZ::ReflectContext* context) + void FPSProfilerConfigFile::Reflect(AZ::ReflectContext* context) { if (auto serializeContext = azrtti_cast(context)) { - serializeContext->Class() + serializeContext->Class() ->Version(0) - ->Field("m_OutputFilename", &FPSProfilerConfig::m_OutputFilename) - ->Field("m_AutoSave", &FPSProfilerConfig::m_AutoSave) - ->Field("m_AutoSaveAtFrame", &FPSProfilerConfig::m_AutoSaveAtFrame) - ->Field("m_SaveWithTimestamp", &FPSProfilerConfig::m_SaveWithTimestamp) - ->Field("m_SaveFPSData", &FPSProfilerConfig::m_SaveFpsData) - ->Field("m_SaveCPUData", &FPSProfilerConfig::m_SaveCpuData) - ->Field("m_SaveGPUData", &FPSProfilerConfig::m_SaveGpuData) - ->Field("m_NearZeroPrecision", &FPSProfilerConfig::m_NearZeroPrecision) - ->Field("m_ShowFPS", &FPSProfilerConfig::m_ShowFps); + ->Field("m_OutputFilename", &FPSProfilerConfigFile::m_OutputFilename) + ->Field("m_AutoSave", &FPSProfilerConfigFile::m_AutoSave) + ->Field("m_AutoSaveAtFrame", &FPSProfilerConfigFile::m_AutoSaveAtFrame) + ->Field("m_SaveWithTimestamp", &FPSProfilerConfigFile::m_SaveWithTimestamp) + ->Field("m_SaveFPSData", &FPSProfilerConfigFile::m_SaveFpsData) + ->Field("m_SaveCPUData", &FPSProfilerConfigFile::m_SaveCpuData) + ->Field("m_SaveGPUData", &FPSProfilerConfigFile::m_SaveGpuData) + ->Field("m_NearZeroPrecision", &FPSProfilerConfigFile::m_NearZeroPrecision) + ->Field("m_ShowFPS", &FPSProfilerConfigFile::m_ShowFps); if (AZ::EditContext* editContext = serializeContext->GetEditContext()) { editContext - ->Class("FPS Profiler Configuration", "Tracks FPS, GPU and CPU performance and saves it into .csv") + ->Class( + "FPS Profiler Configuration", "Tracks FPS, GPU and CPU performance and saves it into .csv") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::Category, "Performance") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) @@ -32,21 +33,21 @@ namespace FPSProfiler ->ClassElement(AZ::Edit::ClassElements::Group, "File Settings") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfig::m_OutputFilename, + &FPSProfilerConfigFile::m_OutputFilename, "Csv Save Path", "Select a path where *.csv will be saved.") ->Attribute(AZ::Edit::Attributes::SourceAssetFilterPattern, "*.csv") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfig::m_AutoSave, + &FPSProfilerConfigFile::m_AutoSave, "Auto Save", "When enabled, system will auto save after specified frame occurrence.") ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree) ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfig::m_AutoSaveAtFrame, + &FPSProfilerConfigFile::m_AutoSaveAtFrame, "Auto Save At Frame", "Specify after how many frames system will auto save log.") ->Attribute(AZ::Edit::Attributes::Min, 1) @@ -54,39 +55,39 @@ namespace FPSProfiler AZ::Edit::Attributes::Visibility, [](const void* instance) { - const FPSProfilerConfig* data = reinterpret_cast(instance); + const FPSProfilerConfigFile* data = reinterpret_cast(instance); return data && data->m_AutoSave ? AZ::Edit::PropertyVisibility::Show : AZ::Edit::PropertyVisibility::Hide; }) ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfig::m_SaveWithTimestamp, + &FPSProfilerConfigFile::m_SaveWithTimestamp, "Timestamp", "When enabled, system will save files with timestamp postfix of current date and hour.") ->ClassElement(AZ::Edit::ClassElements::Group, "Statistics Settings") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfig::m_SaveFpsData, + &FPSProfilerConfigFile::m_SaveFpsData, "Save FPS Data", "When enabled, system will collect FPS data into csv.") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfig::m_SaveGpuData, + &FPSProfilerConfigFile::m_SaveGpuData, "Save GPU Data", "When enabled, system will collect GPU usage data into csv.") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfig::m_SaveCpuData, + &FPSProfilerConfigFile::m_SaveCpuData, "Save CPU Data", "When enabled, system will collect CPU usage data into csv.") ->ClassElement(AZ::Edit::ClassElements::Group, "Precision Settings") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfig::m_NearZeroPrecision, + &FPSProfilerConfigFile::m_NearZeroPrecision, "Near Zero Precision", "Specify near Zero precision, that will be used for system.") ->Attribute(AZ::Edit::Attributes::Min, 0.0f) @@ -96,7 +97,7 @@ namespace FPSProfiler ->ClassElement(AZ::Edit::ClassElements::Group, "Debug Settings") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfig::m_ShowFps, + &FPSProfilerConfigFile::m_ShowFps, "Show FPS", "When enabled, system will show FPS counter in top-left corner."); } diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h index 42df9f2a..bd486a6d 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h @@ -7,9 +7,15 @@ namespace FPSProfiler { - struct FPSProfilerConfig + enum MovingAverageType { - AZ_TYPE_INFO(FPSProfilerConfig, FPSProfilerDataTypeId); + Simple, + Exponential, + }; + + struct FPSProfilerConfigFile + { + AZ_TYPE_INFO(FPSProfilerConfigFile, FPSProfilerConfigFileTypeId); static void Reflect(AZ::ReflectContext* context); AZ::IO::Path m_OutputFilename = "@user@/fps_log.csv"; @@ -20,6 +26,28 @@ namespace FPSProfiler bool m_SaveGpuData = true; bool m_SaveCpuData = true; float m_NearZeroPrecision = 0.01f; + MovingAverageType m_avgFpsType = MovingAverageType::Exponential; + bool m_AverageMedianFilter = true; + bool m_ShowFps = true; + }; + + struct FPSProfilerConfigStats + { + bool m_SaveFpsData = true; + bool m_SaveGpuData = true; + bool m_SaveCpuData = true; + }; + + struct FPSProfilerConfigPrecision + { + float m_NearZeroPrecision = 0.01f; + MovingAverageType m_avgFpsType = MovingAverageType::Exponential; + bool m_AverageMedianFilter = true; + }; + + struct FPSProfilerConfigDebug + { bool m_ShowFps = true; }; + } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 3ce35673..3c0227da 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -7,7 +7,7 @@ namespace FPSProfiler { void FPSProfilerEditorSystemComponent::Reflect(AZ::ReflectContext* context) { - FPSProfilerConfig::Reflect(context); + FPSProfilerConfigFile::Reflect(context); if (auto serializeContext = azrtti_cast(context)) { diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h index c27b4257..250023a7 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h @@ -30,7 +30,7 @@ namespace FPSProfiler static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); - FPSProfilerConfig m_configuration; + FPSProfilerConfigFile m_configuration; bool m_profileOnGameStart = false; }; } // namespace FPSProfiler From 567d1b198c05baaad0766bcc44c69aaa0ef49359 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 15:26:56 +0100 Subject: [PATCH 095/175] move to config structs to namespace Config Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 6 +++--- .../Clients/FPSProfilerSystemComponent.cpp | 2 +- .../Clients/FPSProfilerSystemComponent.h | 4 ++-- .../Code/Source/Tools/FPSProfilerConfig.cpp | 4 ++-- .../Code/Source/Tools/FPSProfilerConfig.h | 20 ++++++++++++++----- .../FPSProfilerEditorSystemComponent.cpp | 2 +- .../Tools/FPSProfilerEditorSystemComponent.h | 2 +- 7 files changed, 25 insertions(+), 15 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index d43bb4d0..46909fd8 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -164,7 +164,7 @@ namespace FPSProfiler * @brief Called when the profiling process starts. * @param config The configuration settings used for the profiling session. */ - virtual void OnProfileStart(const FPSProfilerConfigFile& config) + virtual void OnProfileStart(const Config::FPSProfilerConfigFile& config) { } @@ -172,7 +172,7 @@ namespace FPSProfiler * @brief Called when the profiling data is reset. * @param config The configuration settings used for the profiling session. */ - virtual void OnProfileReset(const FPSProfilerConfigFile& config) + virtual void OnProfileReset(const Config::FPSProfilerConfigFile& config) { } @@ -180,7 +180,7 @@ namespace FPSProfiler * @brief Called when the profiling process stops. * @param config The configuration settings used for the profiling session. */ - virtual void OnProfileStop(const FPSProfilerConfigFile& config) + virtual void OnProfileStop(const Config::FPSProfilerConfigFile& config) { } }; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 9705aadc..d9ebf981 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -38,7 +38,7 @@ namespace FPSProfiler } } - FPSProfilerSystemComponent::FPSProfilerSystemComponent(const FPSProfilerConfigFile& config, bool profileOnGameStart) + FPSProfilerSystemComponent::FPSProfilerSystemComponent(const Config::FPSProfilerConfigFile& config, bool profileOnGameStart) : m_configuration(AZStd::move(config)) , m_profileOnGameStart(profileOnGameStart) { diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index b19d445d..b18e7540 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -23,7 +23,7 @@ namespace FPSProfiler static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); FPSProfilerSystemComponent(); - explicit FPSProfilerSystemComponent(const FPSProfilerConfigFile& config, bool profileOnGameStart); + explicit FPSProfilerSystemComponent(const Config::FPSProfilerConfigFile& config, bool profileOnGameStart); ~FPSProfilerSystemComponent() override; protected: @@ -55,7 +55,7 @@ namespace FPSProfiler private: // Profiler Configuration - FPSProfilerConfigFile m_configuration; //!< Stores editor settings for the profiler + Config::FPSProfilerConfigFile m_configuration; //!< Stores editor settings for the profiler // Profiling State bool m_isProfiling = false; //!< Flag to indicate if profiling is active diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index 453ade3b..f79a56d6 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -2,7 +2,7 @@ #include -namespace FPSProfiler +namespace FPSProfiler::Config { void FPSProfilerConfigFile::Reflect(AZ::ReflectContext* context) { @@ -103,4 +103,4 @@ namespace FPSProfiler } } } -} // namespace FPSProfiler \ No newline at end of file +} // namespace FPSProfiler::Config \ No newline at end of file diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h index bd486a6d..a3d6bacf 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h @@ -2,10 +2,11 @@ #include +#include #include #include -namespace FPSProfiler +namespace FPSProfiler::Config { enum MovingAverageType { @@ -13,6 +14,14 @@ namespace FPSProfiler Exponential, }; + enum RecordStats : u_int8_t + { + None = 1 << 0, + FPS = 1 << 1, + CPU = 1 << 2, + GPU = 1 << 3, + }; + struct FPSProfilerConfigFile { AZ_TYPE_INFO(FPSProfilerConfigFile, FPSProfilerConfigFileTypeId); @@ -31,23 +40,24 @@ namespace FPSProfiler bool m_ShowFps = true; }; - struct FPSProfilerConfigStats + struct Stats { bool m_SaveFpsData = true; bool m_SaveGpuData = true; bool m_SaveCpuData = true; }; - struct FPSProfilerConfigPrecision + struct Precision { float m_NearZeroPrecision = 0.01f; MovingAverageType m_avgFpsType = MovingAverageType::Exponential; bool m_AverageMedianFilter = true; }; - struct FPSProfilerConfigDebug + struct Debug { bool m_ShowFps = true; + AZ::Color m_Color = AZ::Colors::Blue; }; -} // namespace FPSProfiler +} // namespace FPSProfiler::Config diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 3c0227da..008c1ab2 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -7,7 +7,7 @@ namespace FPSProfiler { void FPSProfilerEditorSystemComponent::Reflect(AZ::ReflectContext* context) { - FPSProfilerConfigFile::Reflect(context); + Config::FPSProfilerConfigFile::Reflect(context); if (auto serializeContext = azrtti_cast(context)) { diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h index 250023a7..975dd8e3 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h @@ -30,7 +30,7 @@ namespace FPSProfiler static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); - FPSProfilerConfigFile m_configuration; + Config::FPSProfilerConfigFile m_configuration; bool m_profileOnGameStart = false; }; } // namespace FPSProfiler From 93bb8090e2594b5ae5559a2e241371fde9672ce7 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 15:54:00 +0100 Subject: [PATCH 096/175] rename config | add multiple config strutcs | add type ids Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 6 +- .../Include/FPSProfiler/FPSProfilerTypeIds.h | 5 ++ .../Clients/FPSProfilerSystemComponent.cpp | 2 +- .../Clients/FPSProfilerSystemComponent.h | 4 +- .../Code/Source/Tools/FPSProfilerConfig.cpp | 48 ++++++++-------- .../Code/Source/Tools/FPSProfilerConfig.h | 56 +++++++++++-------- .../FPSProfilerEditorSystemComponent.cpp | 2 +- .../Tools/FPSProfilerEditorSystemComponent.h | 2 +- 8 files changed, 71 insertions(+), 54 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 46909fd8..043edb28 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -164,7 +164,7 @@ namespace FPSProfiler * @brief Called when the profiling process starts. * @param config The configuration settings used for the profiling session. */ - virtual void OnProfileStart(const Config::FPSProfilerConfigFile& config) + virtual void OnProfileStart(const Configs::FileSaveSettings& config) { } @@ -172,7 +172,7 @@ namespace FPSProfiler * @brief Called when the profiling data is reset. * @param config The configuration settings used for the profiling session. */ - virtual void OnProfileReset(const Config::FPSProfilerConfigFile& config) + virtual void OnProfileReset(const Configs::FileSaveSettings& config) { } @@ -180,7 +180,7 @@ namespace FPSProfiler * @brief Called when the profiling process stops. * @param config The configuration settings used for the profiling session. */ - virtual void OnProfileStop(const Config::FPSProfilerConfigFile& config) + virtual void OnProfileStop(const Configs::FileSaveSettings& config) { } }; diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h index 3a3e6295..0d3d13fb 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h @@ -6,7 +6,12 @@ namespace FPSProfiler // System Component TypeIds inline constexpr const char* FPSProfilerSystemComponentTypeId = "{B11CE88E-E1C5-404F-83D2-0D3850445A13}"; inline constexpr const char* FPSProfilerEditorSystemComponentTypeId = "{F4308920-CD0B-4A2E-91DE-2EC1E970F97A}"; + + // Configs TypeIds inline constexpr const char* FPSProfilerConfigFileTypeId = "{70857242-4363-403C-ACF1-4A401B1024B5}"; + inline constexpr const char* FPSProfilerConfigRecordTypeId = "{3CD7901E-F8C2-493A-B182-7EB2BAD9FBFB}"; + inline constexpr const char* FPSProfilerConfigPrecisionTypeId = "{0ADE9CF6-1FE2-49E5-AB8D-B240EBDB9C03}"; + inline constexpr const char* FPSProfilerConfigDebugTypeId = "{974F1627-C476-4310-B72A-7842BF868EC2}"; // Module derived classes TypeIds inline constexpr const char* FPSProfilerModuleInterfaceTypeId = "{77EF155C-6E75-41B1-A939-AF5E2FE4FC6B}"; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index d9ebf981..246259ca 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -38,7 +38,7 @@ namespace FPSProfiler } } - FPSProfilerSystemComponent::FPSProfilerSystemComponent(const Config::FPSProfilerConfigFile& config, bool profileOnGameStart) + FPSProfilerSystemComponent::FPSProfilerSystemComponent(const Configs::FileSaveSettings& config, bool profileOnGameStart) : m_configuration(AZStd::move(config)) , m_profileOnGameStart(profileOnGameStart) { diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index b18e7540..0610c208 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -23,7 +23,7 @@ namespace FPSProfiler static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); FPSProfilerSystemComponent(); - explicit FPSProfilerSystemComponent(const Config::FPSProfilerConfigFile& config, bool profileOnGameStart); + explicit FPSProfilerSystemComponent(const Configs::FileSaveSettings& config, bool profileOnGameStart); ~FPSProfilerSystemComponent() override; protected: @@ -55,7 +55,7 @@ namespace FPSProfiler private: // Profiler Configuration - Config::FPSProfilerConfigFile m_configuration; //!< Stores editor settings for the profiler + Configs::FileSaveSettings m_configuration; //!< Stores editor settings for the profiler // Profiling State bool m_isProfiling = false; //!< Flag to indicate if profiling is active diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index f79a56d6..e2965e8c 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -2,28 +2,28 @@ #include -namespace FPSProfiler::Config +namespace FPSProfiler::Configs { - void FPSProfilerConfigFile::Reflect(AZ::ReflectContext* context) + void FileSaveSettings::Reflect(AZ::ReflectContext* context) { if (auto serializeContext = azrtti_cast(context)) { - serializeContext->Class() + serializeContext->Class() ->Version(0) - ->Field("m_OutputFilename", &FPSProfilerConfigFile::m_OutputFilename) - ->Field("m_AutoSave", &FPSProfilerConfigFile::m_AutoSave) - ->Field("m_AutoSaveAtFrame", &FPSProfilerConfigFile::m_AutoSaveAtFrame) - ->Field("m_SaveWithTimestamp", &FPSProfilerConfigFile::m_SaveWithTimestamp) - ->Field("m_SaveFPSData", &FPSProfilerConfigFile::m_SaveFpsData) - ->Field("m_SaveCPUData", &FPSProfilerConfigFile::m_SaveCpuData) - ->Field("m_SaveGPUData", &FPSProfilerConfigFile::m_SaveGpuData) - ->Field("m_NearZeroPrecision", &FPSProfilerConfigFile::m_NearZeroPrecision) - ->Field("m_ShowFPS", &FPSProfilerConfigFile::m_ShowFps); + ->Field("m_OutputFilename", &FileSaveSettings::m_OutputFilename) + ->Field("m_AutoSave", &FileSaveSettings::m_AutoSave) + ->Field("m_AutoSaveAtFrame", &FileSaveSettings::m_AutoSaveAtFrame) + ->Field("m_SaveWithTimestamp", &FileSaveSettings::m_SaveWithTimestamp) + ->Field("m_SaveFPSData", &FileSaveSettings::m_SaveFpsData) + ->Field("m_SaveCPUData", &FileSaveSettings::m_SaveCpuData) + ->Field("m_SaveGPUData", &FileSaveSettings::m_SaveGpuData) + ->Field("m_NearZeroPrecision", &FileSaveSettings::m_NearZeroPrecision) + ->Field("m_ShowFPS", &FileSaveSettings::m_ShowFps); if (AZ::EditContext* editContext = serializeContext->GetEditContext()) { editContext - ->Class( + ->Class( "FPS Profiler Configuration", "Tracks FPS, GPU and CPU performance and saves it into .csv") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::Category, "Performance") @@ -33,21 +33,21 @@ namespace FPSProfiler::Config ->ClassElement(AZ::Edit::ClassElements::Group, "File Settings") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfigFile::m_OutputFilename, + &FileSaveSettings::m_OutputFilename, "Csv Save Path", "Select a path where *.csv will be saved.") ->Attribute(AZ::Edit::Attributes::SourceAssetFilterPattern, "*.csv") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfigFile::m_AutoSave, + &FileSaveSettings::m_AutoSave, "Auto Save", "When enabled, system will auto save after specified frame occurrence.") ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree) ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfigFile::m_AutoSaveAtFrame, + &FileSaveSettings::m_AutoSaveAtFrame, "Auto Save At Frame", "Specify after how many frames system will auto save log.") ->Attribute(AZ::Edit::Attributes::Min, 1) @@ -55,39 +55,39 @@ namespace FPSProfiler::Config AZ::Edit::Attributes::Visibility, [](const void* instance) { - const FPSProfilerConfigFile* data = reinterpret_cast(instance); + const FileSaveSettings* data = reinterpret_cast(instance); return data && data->m_AutoSave ? AZ::Edit::PropertyVisibility::Show : AZ::Edit::PropertyVisibility::Hide; }) ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfigFile::m_SaveWithTimestamp, + &FileSaveSettings::m_SaveWithTimestamp, "Timestamp", "When enabled, system will save files with timestamp postfix of current date and hour.") ->ClassElement(AZ::Edit::ClassElements::Group, "Statistics Settings") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfigFile::m_SaveFpsData, + &FileSaveSettings::m_SaveFpsData, "Save FPS Data", "When enabled, system will collect FPS data into csv.") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfigFile::m_SaveGpuData, + &FileSaveSettings::m_SaveGpuData, "Save GPU Data", "When enabled, system will collect GPU usage data into csv.") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfigFile::m_SaveCpuData, + &FileSaveSettings::m_SaveCpuData, "Save CPU Data", "When enabled, system will collect CPU usage data into csv.") ->ClassElement(AZ::Edit::ClassElements::Group, "Precision Settings") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfigFile::m_NearZeroPrecision, + &FileSaveSettings::m_NearZeroPrecision, "Near Zero Precision", "Specify near Zero precision, that will be used for system.") ->Attribute(AZ::Edit::Attributes::Min, 0.0f) @@ -97,10 +97,10 @@ namespace FPSProfiler::Config ->ClassElement(AZ::Edit::ClassElements::Group, "Debug Settings") ->DataElement( AZ::Edit::UIHandlers::Default, - &FPSProfilerConfigFile::m_ShowFps, + &FileSaveSettings::m_ShowFps, "Show FPS", "When enabled, system will show FPS counter in top-left corner."); } } } -} // namespace FPSProfiler::Config \ No newline at end of file +} // namespace FPSProfiler::Configs \ No newline at end of file diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h index a3d6bacf..dfa7e925 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h @@ -6,58 +6,70 @@ #include #include -namespace FPSProfiler::Config +namespace FPSProfiler::Configs { - enum MovingAverageType + enum MovingAverageType : bool { - Simple, - Exponential, + Simple = true, + Exponential = false, }; - enum RecordStats : u_int8_t + enum RecordStatistics : uint8_t { None = 1 << 0, FPS = 1 << 1, CPU = 1 << 2, GPU = 1 << 3, + All = FPS | CPU | GPU, }; - struct FPSProfilerConfigFile + enum RecordType { - AZ_TYPE_INFO(FPSProfilerConfigFile, FPSProfilerConfigFileTypeId); + GameStart = 0, + FramePick = 1, + Await = 2, + }; + + struct FileSaveSettings + { + AZ_TYPE_INFO(FileSaveSettings, FPSProfilerConfigFileTypeId); static void Reflect(AZ::ReflectContext* context); AZ::IO::Path m_OutputFilename = "@user@/fps_log.csv"; bool m_AutoSave = true; int m_AutoSaveAtFrame = 100; bool m_SaveWithTimestamp = true; - bool m_SaveFpsData = true; - bool m_SaveGpuData = true; - bool m_SaveCpuData = true; - float m_NearZeroPrecision = 0.01f; - MovingAverageType m_avgFpsType = MovingAverageType::Exponential; - bool m_AverageMedianFilter = true; - bool m_ShowFps = true; }; - struct Stats + struct RecordSettings { - bool m_SaveFpsData = true; - bool m_SaveGpuData = true; - bool m_SaveCpuData = true; + AZ_TYPE_INFO(RecordSettings, FPSProfilerConfigRecordTypeId); + static void Reflect(AZ::ReflectContext* context); + + RecordType m_recordType = RecordType::GameStart; + float m_framesToSkip = 0.0f; // Available only for FramePick + float m_framesToRecord = 0.0f; + RecordStatistics m_RecordStats = RecordStatistics::All; }; - struct Precision + struct PrecisionSettings { + AZ_TYPE_INFO(PrecisionSettings, FPSProfilerConfigPrecisionTypeId); + static void Reflect(AZ::ReflectContext* context); + float m_NearZeroPrecision = 0.01f; MovingAverageType m_avgFpsType = MovingAverageType::Exponential; - bool m_AverageMedianFilter = true; + bool m_useAvgMedianFilter = true; }; - struct Debug + struct DebugSettings { + AZ_TYPE_INFO(DebugSettings, FPSProfilerConfigDebugTypeId); + static void Reflect(AZ::ReflectContext* context); + + bool m_PrintDebugInfo = true; bool m_ShowFps = true; AZ::Color m_Color = AZ::Colors::Blue; }; -} // namespace FPSProfiler::Config +} // namespace FPSProfiler::Configs diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 008c1ab2..1ddfce4d 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -7,7 +7,7 @@ namespace FPSProfiler { void FPSProfilerEditorSystemComponent::Reflect(AZ::ReflectContext* context) { - Config::FPSProfilerConfigFile::Reflect(context); + Configs::FileSaveSettings::Reflect(context); if (auto serializeContext = azrtti_cast(context)) { diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h index 975dd8e3..c4562cf1 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h @@ -30,7 +30,7 @@ namespace FPSProfiler static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); - Config::FPSProfilerConfigFile m_configuration; + Configs::FileSaveSettings m_configuration; bool m_profileOnGameStart = false; }; } // namespace FPSProfiler From 4c4537a016dac0f318264c0e8b13fe90c8d21f51 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 16:01:20 +0100 Subject: [PATCH 097/175] fix mask bit shifts Signed-off-by: Wojciech Czerski --- .../Code/Source/Tools/FPSProfilerConfig.h | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h index dfa7e925..102ad907 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h @@ -14,15 +14,6 @@ namespace FPSProfiler::Configs Exponential = false, }; - enum RecordStatistics : uint8_t - { - None = 1 << 0, - FPS = 1 << 1, - CPU = 1 << 2, - GPU = 1 << 3, - All = FPS | CPU | GPU, - }; - enum RecordType { GameStart = 0, @@ -30,6 +21,15 @@ namespace FPSProfiler::Configs Await = 2, }; + enum RecordStatistics : uint8_t + { + None = 0, + FPS = 1 << 0, + CPU = 1 << 1, + GPU = 1 << 2, + All = FPS | CPU | GPU, + }; + struct FileSaveSettings { AZ_TYPE_INFO(FileSaveSettings, FPSProfilerConfigFileTypeId); From 5b9928b9bff0f0bbea3e8e96b58b6c134c2c7feb Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 16:03:40 +0100 Subject: [PATCH 098/175] extra option Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h index 102ad907..8a5c7173 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h @@ -28,6 +28,7 @@ namespace FPSProfiler::Configs CPU = 1 << 1, GPU = 1 << 2, All = FPS | CPU | GPU, + Memory = CPU | GPU, }; struct FileSaveSettings From b18e30bf6a6d2318827995e3e64fb4ad7f093fca Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 16:04:48 +0100 Subject: [PATCH 099/175] rename Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h index 8a5c7173..e2583e8e 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h @@ -28,7 +28,7 @@ namespace FPSProfiler::Configs CPU = 1 << 1, GPU = 1 << 2, All = FPS | CPU | GPU, - Memory = CPU | GPU, + MemoryUsage = CPU | GPU, }; struct FileSaveSettings From 3d3e88e0227517107aaac5bc9aae127c7c3893c6 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 16:40:47 +0100 Subject: [PATCH 100/175] create reflect for RecrodSettings Signed-off-by: Wojciech Czerski --- .../Code/Source/Tools/FPSProfilerConfig.cpp | 151 ++++++++++++++---- .../Code/Source/Tools/FPSProfilerConfig.h | 18 ++- 2 files changed, 129 insertions(+), 40 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index e2965e8c..8efd9686 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -13,21 +13,13 @@ namespace FPSProfiler::Configs ->Field("m_OutputFilename", &FileSaveSettings::m_OutputFilename) ->Field("m_AutoSave", &FileSaveSettings::m_AutoSave) ->Field("m_AutoSaveAtFrame", &FileSaveSettings::m_AutoSaveAtFrame) - ->Field("m_SaveWithTimestamp", &FileSaveSettings::m_SaveWithTimestamp) - ->Field("m_SaveFPSData", &FileSaveSettings::m_SaveFpsData) - ->Field("m_SaveCPUData", &FileSaveSettings::m_SaveCpuData) - ->Field("m_SaveGPUData", &FileSaveSettings::m_SaveGpuData) - ->Field("m_NearZeroPrecision", &FileSaveSettings::m_NearZeroPrecision) - ->Field("m_ShowFPS", &FileSaveSettings::m_ShowFps); + ->Field("m_SaveWithTimestamp", &FileSaveSettings::m_SaveWithTimestamp); if (AZ::EditContext* editContext = serializeContext->GetEditContext()) { editContext - ->Class( - "FPS Profiler Configuration", "Tracks FPS, GPU and CPU performance and saves it into .csv") + ->Class("FPS Profiler Configuration", "Tracks FPS, GPU and CPU performance and saves it into .csv") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") - ->Attribute(AZ::Edit::Attributes::Category, "Performance") - ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->ClassElement(AZ::Edit::ClassElements::Group, "File Settings") @@ -55,7 +47,7 @@ namespace FPSProfiler::Configs AZ::Edit::Attributes::Visibility, [](const void* instance) { - const FileSaveSettings* data = reinterpret_cast(instance); + const FileSaveSettings* data = static_cast(instance); return data && data->m_AutoSave ? AZ::Edit::PropertyVisibility::Show : AZ::Edit::PropertyVisibility::Hide; }) @@ -63,44 +55,137 @@ namespace FPSProfiler::Configs AZ::Edit::UIHandlers::Default, &FileSaveSettings::m_SaveWithTimestamp, "Timestamp", - "When enabled, system will save files with timestamp postfix of current date and hour.") + "When enabled, system will save files with timestamp postfix of current date and hour."); + } + } + } + + void RecordSettings::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ->Field("m_recordType", &RecordSettings::m_recordType) + ->Field("m_framesToSkip", &RecordSettings::m_framesToSkip) + ->Field("m_framesToRecord", &RecordSettings::m_framesToRecord) + ->Field("m_RecordStats", &RecordSettings::m_RecordStats); + + if (auto* editContext = serializeContext->GetEditContext()) + { + editContext->Class("Record Settings", "Settings controlling the recording behavior.") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) - ->ClassElement(AZ::Edit::ClassElements::Group, "Statistics Settings") + // Reflect enum with ComboBox: ->DataElement( - AZ::Edit::UIHandlers::Default, - &FileSaveSettings::m_SaveFpsData, - "Save FPS Data", - "When enabled, system will collect FPS data into csv.") + AZ::Edit::UIHandlers::ComboBox, &RecordSettings::m_recordType, "Record Type", "Specifies the type of record.") + // Provide the combo‐box choices: + ->EnumAttribute(static_cast(RecordType::GameStart), "Game Start") + ->EnumAttribute(static_cast(RecordType::FramePick), "Frame Pick") + ->EnumAttribute(static_cast(RecordType::Await), "Await") + // Ensure the UI updates when the enum changes: + ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree) + // Conditionally show m_framesToSkip only if "Frame Pick" is selected: ->DataElement( AZ::Edit::UIHandlers::Default, - &FileSaveSettings::m_SaveGpuData, - "Save GPU Data", - "When enabled, system will collect GPU usage data into csv.") + &RecordSettings::m_framesToSkip, + "Frames To Skip", + "Number of frames to skip before starting recording.") + ->Attribute( + AZ::Edit::Attributes::Visibility, + [](const void* instance) + { + const RecordSettings* data = static_cast(instance); + return data && data->m_recordType == RecordType::FramePick ? AZ::Edit::PropertyVisibility::Show + : AZ::Edit::PropertyVisibility::Hide; + }) ->DataElement( AZ::Edit::UIHandlers::Default, - &FileSaveSettings::m_SaveCpuData, - "Save CPU Data", - "When enabled, system will collect CPU usage data into csv.") + &RecordSettings::m_recordType, + "Record Type", + "Specifies the type of desired record.") - ->ClassElement(AZ::Edit::ClassElements::Group, "Precision Settings") ->DataElement( AZ::Edit::UIHandlers::Default, - &FileSaveSettings::m_NearZeroPrecision, - "Near Zero Precision", - "Specify near Zero precision, that will be used for system.") + &RecordSettings::m_framesToSkip, + "Frames To Skip", + "Number of frames to skip before starting recording.") ->Attribute(AZ::Edit::Attributes::Min, 0.0f) - ->Attribute(AZ::Edit::Attributes::Max, 0.1f) - ->Attribute(AZ::Edit::Attributes::Step, 0.00001f) + ->Attribute(AZ::Edit::Attributes::Step, 1.0f) - ->ClassElement(AZ::Edit::ClassElements::Group, "Debug Settings") ->DataElement( AZ::Edit::UIHandlers::Default, - &FileSaveSettings::m_ShowFps, - "Show FPS", - "When enabled, system will show FPS counter in top-left corner."); + &RecordSettings::m_framesToRecord, + "Frames To Record", + "Number of frames to capture. If set to 0.0f it will be skipped.") + ->Attribute(AZ::Edit::Attributes::Min, 0.0f) + ->Attribute(AZ::Edit::Attributes::Step, 100.0f) + + // FPS Button + ->UIElement(AZ::Edit::UIHandlers::Button, "Toggle Save FPS", "Toggle FPS recording") + ->Attribute( + AZ::Edit::Attributes::ButtonText, + [](void* instance) -> AZStd::string + { + auto* self = reinterpret_cast(instance); + return (self->m_RecordStats & RecordStatistics::FPS) ? "Disable FPS" : "Enable FPS"; + }) + ->Attribute( + AZ::Edit::Attributes::ChangeNotify, + [](void* instance) + { + auto* self = reinterpret_cast(instance); + self->m_RecordStats = static_cast(self->m_RecordStats ^ RecordStatistics::FPS); // Toggle bit + return AZ::Edit::PropertyRefreshLevels::ValuesOnly; + }) + + // CPU Button + ->UIElement(AZ::Edit::UIHandlers::Button, "Toggle Save CPU", "Toggle CPU recording") + ->Attribute( + AZ::Edit::Attributes::ButtonText, + [](void* instance) -> AZStd::string + { + auto* self = reinterpret_cast(instance); + return (self->m_RecordStats & RecordStatistics::CPU) ? "Disable CPU" : "Enable CPU"; + }) + ->Attribute( + AZ::Edit::Attributes::ChangeNotify, + [](void* instance) + { + auto* self = reinterpret_cast(instance); + self->m_RecordStats = static_cast(self->m_RecordStats ^ RecordStatistics::CPU); // Toggle bit + return AZ::Edit::PropertyRefreshLevels::ValuesOnly; + }) + + // GPU Button + ->UIElement(AZ::Edit::UIHandlers::Button, "Save Save GPU", "Toggle GPU recording") + ->Attribute( + AZ::Edit::Attributes::ButtonText, + [](void* instance) -> AZStd::string + { + auto* self = reinterpret_cast(instance); + return (self->m_RecordStats & RecordStatistics::GPU) ? "Disable GPU" : "Enable GPU"; + }) + ->Attribute( + AZ::Edit::Attributes::ChangeNotify, + [](void* instance) + { + auto* self = reinterpret_cast(instance); + self->m_RecordStats = static_cast(self->m_RecordStats ^ RecordStatistics::GPU); // Toggle bit + return AZ::Edit::PropertyRefreshLevels::ValuesOnly; + }); } } } + + void PrecisionSettings::Reflect(AZ::ReflectContext* context) + { + } + + void DebugSettings::Reflect(AZ::ReflectContext* context) + { + } } // namespace FPSProfiler::Configs \ No newline at end of file diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h index e2583e8e..155fbaee 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h @@ -10,9 +10,10 @@ namespace FPSProfiler::Configs { enum MovingAverageType : bool { - Simple = true, - Exponential = false, + Simple = 0, + Exponential = 1, }; + AZ_DEFINE_ENUM_RELATIONAL_OPERATORS(MovingAverageType); enum RecordType { @@ -20,16 +21,19 @@ namespace FPSProfiler::Configs FramePick = 1, Await = 2, }; + AZ_DEFINE_ENUM_RELATIONAL_OPERATORS(RecordType); enum RecordStatistics : uint8_t { None = 0, - FPS = 1 << 0, - CPU = 1 << 1, - GPU = 1 << 2, - All = FPS | CPU | GPU, + FPS = 1 << 0, + CPU = 1 << 1, + GPU = 1 << 2, + All = FPS | CPU | GPU, MemoryUsage = CPU | GPU, }; + AZ_DEFINE_ENUM_BITWISE_OPERATORS(RecordStatistics); + AZ_DEFINE_ENUM_RELATIONAL_OPERATORS(RecordStatistics); struct FileSaveSettings { @@ -48,7 +52,7 @@ namespace FPSProfiler::Configs static void Reflect(AZ::ReflectContext* context); RecordType m_recordType = RecordType::GameStart; - float m_framesToSkip = 0.0f; // Available only for FramePick + float m_framesToSkip = 0.0f; // Available only for FramePick float m_framesToRecord = 0.0f; RecordStatistics m_RecordStats = RecordStatistics::All; }; From c776279eedf4c03e5b2fb0b51974ee3e64c7388e Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 16:44:36 +0100 Subject: [PATCH 101/175] add other config refelcetions Signed-off-by: Wojciech Czerski --- .../Code/Source/Tools/FPSProfilerConfig.cpp | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index 8efd9686..9bcbb2e0 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -183,9 +183,64 @@ namespace FPSProfiler::Configs void PrecisionSettings::Reflect(AZ::ReflectContext* context) { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Enum() + ->Value("Simple", MovingAverageType::Simple) + ->Value("Exponential", MovingAverageType::Exponential); + + serializeContext->Class() + ->Version(1) + ->Field("NearZeroPrecision", &PrecisionSettings::m_NearZeroPrecision) + ->Field("AverageFpsType", &PrecisionSettings::m_avgFpsType) + ->Field("UseAverageMedianFilter", &PrecisionSettings::m_useAvgMedianFilter); + + if (auto* editContext = serializeContext->GetEditContext()) + { + editContext->Class("Precision Settings", "Settings for FPS profiler precision") + ->DataElement( + AZ::Edit::UIHandlers::Default, + &PrecisionSettings::m_NearZeroPrecision, + "Near Zero Precision", + "Threshold for near-zero values") + ->DataElement( + AZ::Edit::UIHandlers::ComboBox, + &PrecisionSettings::m_avgFpsType, + "Average FPS Type", + "Select the type of moving average to use") + ->EnumAttribute(MovingAverageType::Simple, "Simple Moving Average") + ->EnumAttribute(MovingAverageType::Exponential, "Exponential Moving Average") + ->DataElement( + AZ::Edit::UIHandlers::CheckBox, + &PrecisionSettings::m_useAvgMedianFilter, + "Use Average Median Filter", + "Enable median filtering for averaging"); + } + } } void DebugSettings::Reflect(AZ::ReflectContext* context) { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("PrintDebugInfo", &DebugSettings::m_PrintDebugInfo) + ->Field("ShowFps", &DebugSettings::m_ShowFps) + ->Field("Color", &DebugSettings::m_Color); + + if (auto* editContext = serializeContext->GetEditContext()) + { + editContext->Class("Debug Settings", "Settings for debugging the FPS Profiler") + ->DataElement( + AZ::Edit::UIHandlers::CheckBox, + &DebugSettings::m_PrintDebugInfo, + "Print Debug Info", + "Enable or disable debug information printing") + ->DataElement(AZ::Edit::UIHandlers::CheckBox, &DebugSettings::m_ShowFps, "Show FPS", "Toggle FPS display on screen") + ->DataElement( + AZ::Edit::UIHandlers::Color, &DebugSettings::m_Color, "Debug Color", "Set the debug information display color"); + } + } } } // namespace FPSProfiler::Configs \ No newline at end of file From 584c283949b9f4ac8a706f88288aeba40634f878 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 16:53:06 +0100 Subject: [PATCH 102/175] add new config to system component Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 62 +++++++++---------- .../Clients/FPSProfilerSystemComponent.h | 7 ++- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 246259ca..b72d5bce 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -15,7 +15,7 @@ namespace FPSProfiler { serializeContext->Class() ->Version(0) - ->Field("m_Configuration", &FPSProfilerSystemComponent::m_configuration) + ->Field("m_Configuration", &FPSProfilerSystemComponent::m_configFile) ->Field("m_profileOnGameStart", &FPSProfilerSystemComponent::m_profileOnGameStart); } } @@ -39,7 +39,7 @@ namespace FPSProfiler } FPSProfilerSystemComponent::FPSProfilerSystemComponent(const Configs::FileSaveSettings& config, bool profileOnGameStart) - : m_configuration(AZStd::move(config)) + : m_configFile(AZStd::move(config)) , m_profileOnGameStart(profileOnGameStart) { if (FPSProfilerInterface::Get() == nullptr) @@ -58,18 +58,18 @@ namespace FPSProfiler void FPSProfilerSystemComponent::Activate() { - if (!IsPathValid(m_configuration.m_OutputFilename)) + if (!IsPathValid(m_configFile.m_OutputFilename)) { - m_configuration.m_OutputFilename = "@user@/fps_log.csv"; - AZ_Warning("FPSProfiler", false, "Invalid output file path. Using default: %s", m_configuration.m_OutputFilename.c_str()); + m_configFile.m_OutputFilename = "@user@/fps_log.csv"; + AZ_Warning("FPSProfiler", false, "Invalid output file path. Using default: %s", m_configFile.m_OutputFilename.c_str()); } FPSProfilerRequestBus::Handler::BusConnect(); // connect first to broadcast notifications AZ::TickBus::Handler::BusConnect(); // connect last, after setup // Reserve log entries buffer size based on known auto save per frame - m_configuration.m_AutoSave ? m_logBuffer.reserve(MAX_LOG_BUFFER_LINE_SIZE * m_configuration.m_AutoSaveAtFrame * 2) - : m_logBuffer.reserve(MAX_LOG_BUFFER_SIZE); + m_configFile.m_AutoSave ? m_logBuffer.reserve(MAX_LOG_BUFFER_LINE_SIZE * m_configFile.m_AutoSaveAtFrame * 2) + : m_logBuffer.reserve(MAX_LOG_BUFFER_SIZE); if (m_profileOnGameStart) { @@ -89,7 +89,7 @@ namespace FPSProfiler WriteDataToFile(); // Notify - File Saved - FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileSaved, m_configuration.m_OutputFilename.c_str()); + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileSaved, m_configFile.m_OutputFilename.c_str()); FPSProfilerRequestBus::Handler::BusDisconnect(); } @@ -103,7 +103,7 @@ namespace FPSProfiler // Update FPS data CalculateFpsData(deltaTime); - if (m_configuration.m_ShowFps) + if (m_configDebug.m_ShowFps) { ShowFps(); } @@ -122,21 +122,21 @@ namespace FPSProfiler float usedGpu = -1.0f, reservedGpu = -1.0f; const char* logEntryFormat = "-1,-1.0,-1.0,-1.0,-1.0,-1.0,%.2f,%.2f,%.2f,%.2f\n"; - if (m_configuration.m_SaveCpuData) + if (m_configRecord.m_RecordStats | Configs::RecordStatistics::CPU) { auto [cpuUsed, cpuReserved] = GetCpuMemoryUsed(); usedCpu = BytesToMB(cpuUsed); reservedCpu = BytesToMB(cpuReserved); } - if (m_configuration.m_SaveGpuData) + if (m_configRecord.m_RecordStats | Configs::RecordStatistics::GPU) { auto [gpuUsed, gpuReserved] = GetGpuMemoryUsed(); usedGpu = BytesToMB(gpuUsed); reservedGpu = BytesToMB(gpuReserved); } - if (m_configuration.m_SaveCpuData) + if (m_configRecord.m_RecordStats | Configs::RecordStatistics::FPS) { logEntryFormat = "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n"; } @@ -159,7 +159,7 @@ namespace FPSProfiler m_logBuffer.insert(m_logBuffer.end(), logEntry, logEntry + logEntryLength); // Auto save - if (m_configuration.m_AutoSave && (m_frameCount % m_configuration.m_AutoSaveAtFrame == 0)) + if (m_configFile.m_AutoSave && (m_frameCount % m_configFile.m_AutoSaveAtFrame == 0)) { WriteDataToFile(); } @@ -189,7 +189,7 @@ namespace FPSProfiler } // Notify - Profile Started - FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileStart, m_configuration); + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileStart, m_configFile); AZ_Printf("FPS Profiler", "Profiling started."); } @@ -211,7 +211,7 @@ namespace FPSProfiler SaveLogToFile(); // Notify - Profile Stopped - FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileStop, m_configuration); + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileStop, m_configFile); AZ_Printf("FPS Profiler", "Profiling stopped."); } @@ -227,7 +227,7 @@ namespace FPSProfiler m_logBuffer.clear(); // Notify - Profile Reset - FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileReset, m_configuration); + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileReset, m_configFile); AZ_Printf("FPS Profiler", "Profiling data reseted."); } @@ -238,7 +238,7 @@ namespace FPSProfiler bool FPSProfilerSystemComponent::IsAnySaveOptionEnabled() const { - return m_configuration.m_SaveFpsData || m_configuration.m_SaveCpuData || m_configuration.m_SaveGpuData; + return !(m_configRecord.m_RecordStats | Configs::RecordStatistics::None); } void FPSProfilerSystemComponent::ChangeSavePath(const AZ::IO::Path& newSavePath) @@ -248,7 +248,7 @@ namespace FPSProfiler return; } - m_configuration.m_OutputFilename = newSavePath; + m_configFile.m_OutputFilename = newSavePath; AZ_Warning("FPS Profiler", !m_isProfiling, "Path changed during activated profiling."); } @@ -326,7 +326,7 @@ namespace FPSProfiler void FPSProfilerSystemComponent::ShowFpsOnScreen(bool enable) { - m_configuration.m_ShowFps = enable; + m_configDebug.m_ShowFps = enable; } void FPSProfilerSystemComponent::CalculateFpsData(const float& deltaTime) @@ -337,7 +337,7 @@ namespace FPSProfiler // Latest fps hisotry for avg fps calculation m_fpsSamples.push_back(m_currentFps); - if (m_fpsSamples.size() > m_configuration.m_AutoSaveAtFrame) + if (m_fpsSamples.size() > m_configFile.m_AutoSaveAtFrame) { m_fpsSamples.pop_front(); } @@ -345,7 +345,7 @@ namespace FPSProfiler m_avgFps = AZStd::accumulate(m_fpsSamples.begin(), m_fpsSamples.end(), 0.0f) / static_cast(m_fpsSamples.size()); // Using m_NearZeroPrecision, since m_currentFPS cannot be equal to 0 if delta time is valid. - if (m_currentFps >= m_configuration.m_NearZeroPrecision) + if (m_currentFps >= m_configPrecision.m_NearZeroPrecision) { m_minFps = AZStd::min(m_minFps, m_currentFps); } @@ -361,14 +361,14 @@ namespace FPSProfiler return; } - if (!IsPathValid(m_configuration.m_OutputFilename)) + if (!IsPathValid(m_configFile.m_OutputFilename)) { - m_configuration.m_OutputFilename = "@user@/fps_log.csv"; - AZ_Warning("FPSProfiler", false, "Invalid output file path. Using default: %s", m_configuration.m_OutputFilename.c_str()); + m_configFile.m_OutputFilename = "@user@/fps_log.csv"; + AZ_Warning("FPSProfiler", false, "Invalid output file path. Using default: %s", m_configFile.m_OutputFilename.c_str()); } // Apply Timestamp - if (m_configuration.m_SaveWithTimestamp) + if (m_configFile.m_SaveWithTimestamp) { auto now = AZStd::chrono::system_clock::now(); std::time_t now_time_t = AZStd::chrono::system_clock::to_time_t(now); @@ -380,20 +380,20 @@ namespace FPSProfiler char timestamp[16]; strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M", &timeInfo); - m_configuration.m_OutputFilename.ReplaceFilename( - (m_configuration.m_OutputFilename.Stem().String() + "_" + timestamp + m_configuration.m_OutputFilename.Extension().String()) + m_configFile.m_OutputFilename.ReplaceFilename( + (m_configFile.m_OutputFilename.Stem().String() + "_" + timestamp + m_configFile.m_OutputFilename.Extension().String()) .data()); } // Write profiling headers to file - AZ::IO::FileIOStream file(m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath); + AZ::IO::FileIOStream file(m_configFile.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath); AZStd::string csvHeader = "Frame,FrameTime,CurrentFPS,MinFPS,MaxFPS,AvgFPS,CpuMemoryUsed,CpuMemoryReserved,GpuMemoryUsed,GpuMemoryReserved\n"; file.Write(csvHeader.size(), csvHeader.c_str()); file.Close(); // Notify - File Created - FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileCreated, m_configuration.m_OutputFilename.c_str()); + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileCreated, m_configFile.m_OutputFilename.c_str()); } void FPSProfilerSystemComponent::WriteDataToFile() @@ -409,7 +409,7 @@ namespace FPSProfiler } AZ::IO::HandleType file; - if (AZ::IO::FileIOBase::GetInstance()->Open(m_configuration.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeAppend, file)) + if (AZ::IO::FileIOBase::GetInstance()->Open(m_configFile.m_OutputFilename.c_str(), AZ::IO::OpenMode::ModeAppend, file)) { AZ::IO::FileIOBase::GetInstance()->Write(file, m_logBuffer.data(), m_logBuffer.size()); AZ::IO::FileIOBase::GetInstance()->Close(file); @@ -417,7 +417,7 @@ namespace FPSProfiler m_logBuffer.clear(); // Notify - File Update - FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileUpdate, m_configuration.m_OutputFilename.c_str()); + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileUpdate, m_configFile.m_OutputFilename.c_str()); } float FPSProfilerSystemComponent::BytesToMB(AZStd::size_t bytes) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 0610c208..6f1caf77 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -54,8 +54,11 @@ namespace FPSProfiler void ShowFpsOnScreen(bool enable) override; private: - // Profiler Configuration - Configs::FileSaveSettings m_configuration; //!< Stores editor settings for the profiler + // Profiler Configurations + Configs::FileSaveSettings m_configFile; //!< Stores editor settings for the profiler + Configs::RecordSettings m_configRecord; //!< Stores editor settings for the profiler + Configs::PrecisionSettings m_configPrecision; //!< Stores editor settings for the profiler + Configs::DebugSettings m_configDebug; //!< Stores editor settings for the profiler // Profiling State bool m_isProfiling = false; //!< Flag to indicate if profiling is active From ef6993abe08ca9d5b0ee0f00d5c60719f4039091 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Wed, 5 Mar 2025 17:15:18 +0100 Subject: [PATCH 103/175] remove enum int cast Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index 9bcbb2e0..ea98bf53 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -81,9 +81,9 @@ namespace FPSProfiler::Configs ->DataElement( AZ::Edit::UIHandlers::ComboBox, &RecordSettings::m_recordType, "Record Type", "Specifies the type of record.") // Provide the combo‐box choices: - ->EnumAttribute(static_cast(RecordType::GameStart), "Game Start") - ->EnumAttribute(static_cast(RecordType::FramePick), "Frame Pick") - ->EnumAttribute(static_cast(RecordType::Await), "Await") + ->EnumAttribute(RecordType::GameStart, "Game Start") + ->EnumAttribute(RecordType::FramePick, "Frame Pick") + ->EnumAttribute(RecordType::Await, "Await") // Ensure the UI updates when the enum changes: ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree) From c9836550ec2c5dcbf5e06900bb4219c69eeea907 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 10:36:11 +0100 Subject: [PATCH 104/175] replace old config | build success Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 36 +++++++++++++++---- .../Clients/FPSProfilerSystemComponent.h | 7 ++-- .../Code/Source/Tools/FPSProfilerConfig.cpp | 4 +-- .../Code/Source/Tools/FPSProfilerConfig.h | 4 +-- .../FPSProfilerEditorSystemComponent.cpp | 22 ++++++------ .../Tools/FPSProfilerEditorSystemComponent.h | 6 ++-- 6 files changed, 54 insertions(+), 25 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index b72d5bce..25d2e427 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -15,8 +15,10 @@ namespace FPSProfiler { serializeContext->Class() ->Version(0) - ->Field("m_Configuration", &FPSProfilerSystemComponent::m_configFile) - ->Field("m_profileOnGameStart", &FPSProfilerSystemComponent::m_profileOnGameStart); + ->Field("m_configFile", &FPSProfilerSystemComponent::m_configFile) + ->Field("m_configRecord", &FPSProfilerSystemComponent::m_configRecord) + ->Field("m_configPrecision", &FPSProfilerSystemComponent::m_configPrecision) + ->Field("m_configDebug", &FPSProfilerSystemComponent::m_configDebug); } } @@ -38,9 +40,15 @@ namespace FPSProfiler } } - FPSProfilerSystemComponent::FPSProfilerSystemComponent(const Configs::FileSaveSettings& config, bool profileOnGameStart) - : m_configFile(AZStd::move(config)) - , m_profileOnGameStart(profileOnGameStart) + FPSProfilerSystemComponent::FPSProfilerSystemComponent( + const Configs::FileSaveSettings& configF, + const Configs::RecordSettings& configS, + const Configs::PrecisionSettings& configP, + const Configs::DebugSettings& configD) + : m_configFile(AZStd::move(configF)) + , m_configRecord(AZStd::move(configS)) + , m_configPrecision(AZStd::move(configP)) + , m_configDebug(AZStd::move(configD)) { if (FPSProfilerInterface::Get() == nullptr) { @@ -71,7 +79,7 @@ namespace FPSProfiler m_configFile.m_AutoSave ? m_logBuffer.reserve(MAX_LOG_BUFFER_LINE_SIZE * m_configFile.m_AutoSaveAtFrame * 2) : m_logBuffer.reserve(MAX_LOG_BUFFER_SIZE); - if (m_profileOnGameStart) + if (m_configRecord.m_recordType == Configs::RecordType::GameStart) { AZ::TickBus::QueueFunction( [this]() @@ -108,6 +116,22 @@ namespace FPSProfiler ShowFps(); } + if (m_configRecord.m_recordType == Configs::RecordType::FramePick && m_configRecord.m_framesToSkip > m_frameCount) + { + // Wait for selected frame + return; + } + + if (m_configRecord.m_framesToRecord <= m_configPrecision.m_NearZeroPrecision) + { + static int localFrameCount = 0; + if (m_configRecord.m_framesToRecord == localFrameCount) + { + StopProfiling(); + } + localFrameCount++; + } + if (!IsAnySaveOptionEnabled()) { return; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 6f1caf77..5514b06b 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -23,7 +23,11 @@ namespace FPSProfiler static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); FPSProfilerSystemComponent(); - explicit FPSProfilerSystemComponent(const Configs::FileSaveSettings& config, bool profileOnGameStart); + explicit FPSProfilerSystemComponent( + const Configs::FileSaveSettings& configF, + const Configs::RecordSettings& configS, + const Configs::PrecisionSettings& configP, + const Configs::DebugSettings& configD); ~FPSProfilerSystemComponent() override; protected: @@ -62,7 +66,6 @@ namespace FPSProfiler // Profiling State bool m_isProfiling = false; //!< Flag to indicate if profiling is active - bool m_profileOnGameStart = false; //!< Should start profiling at game start. // FPS Tracking Data float m_minFps = 0.0f; //!< Lowest FPS value recorded diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index ea98bf53..fe79b0ef 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -17,12 +17,10 @@ namespace FPSProfiler::Configs if (AZ::EditContext* editContext = serializeContext->GetEditContext()) { - editContext - ->Class("FPS Profiler Configuration", "Tracks FPS, GPU and CPU performance and saves it into .csv") + editContext->Class("File Settings", "Settings controlling save file operations.") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) - ->ClassElement(AZ::Edit::ClassElements::Group, "File Settings") ->DataElement( AZ::Edit::UIHandlers::Default, &FileSaveSettings::m_OutputFilename, diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h index 155fbaee..c7adf349 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h @@ -52,7 +52,7 @@ namespace FPSProfiler::Configs static void Reflect(AZ::ReflectContext* context); RecordType m_recordType = RecordType::GameStart; - float m_framesToSkip = 0.0f; // Available only for FramePick + int m_framesToSkip = 0; // Available only for FramePick float m_framesToRecord = 0.0f; RecordStatistics m_RecordStats = RecordStatistics::All; }; @@ -74,7 +74,7 @@ namespace FPSProfiler::Configs bool m_PrintDebugInfo = true; bool m_ShowFps = true; - AZ::Color m_Color = AZ::Colors::Blue; + AZ::Color m_Color = AZ::Colors::DarkRed; }; } // namespace FPSProfiler::Configs diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 1ddfce4d..6a2caae3 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -8,13 +8,18 @@ namespace FPSProfiler void FPSProfilerEditorSystemComponent::Reflect(AZ::ReflectContext* context) { Configs::FileSaveSettings::Reflect(context); + Configs::RecordSettings::Reflect(context); + Configs::PrecisionSettings::Reflect(context); + Configs::DebugSettings::Reflect(context); if (auto serializeContext = azrtti_cast(context)) { serializeContext->Class() ->Version(0) - ->Field("m_Configuration", &FPSProfilerEditorSystemComponent::m_configuration) - ->Field("m_profileOnGameStart", &FPSProfilerEditorSystemComponent::m_profileOnGameStart); + ->Field("m_configFile", &FPSProfilerEditorSystemComponent::m_configFile) + ->Field("m_configRecord", &FPSProfilerEditorSystemComponent::m_configRecord) + ->Field("m_configPrecision", &FPSProfilerEditorSystemComponent::m_configPrecision) + ->Field("m_configDebug", &FPSProfilerEditorSystemComponent::m_configDebug); if (AZ::EditContext* editContext = serializeContext->GetEditContext()) { @@ -24,13 +29,10 @@ namespace FPSProfiler ->Attribute(AZ::Edit::Attributes::Category, "Performance") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) ->Attribute(AZ::Edit::Attributes::AutoExpand, true) - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configuration) - - ->DataElement( - AZ::Edit::UIHandlers::Default, - &FPSProfilerEditorSystemComponent::m_profileOnGameStart, - "Profile On Game Start", - "Should system start profiling data instantly after game is launched, or await for other system to activate it?"); + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configFile) + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configRecord) + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configPrecision) + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configDebug); } } } @@ -57,6 +59,6 @@ namespace FPSProfiler void FPSProfilerEditorSystemComponent::BuildGameEntity(AZ::Entity* entity) { - entity->CreateComponent(m_configuration, m_profileOnGameStart); + entity->CreateComponent(m_configFile, m_configRecord, m_configPrecision, m_configDebug); } } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h index c4562cf1..46f048dd 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h @@ -30,7 +30,9 @@ namespace FPSProfiler static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); - Configs::FileSaveSettings m_configuration; - bool m_profileOnGameStart = false; + Configs::FileSaveSettings m_configFile; //!< Stores editor settings for the profiler + Configs::RecordSettings m_configRecord; //!< Stores editor settings for the profiler + Configs::PrecisionSettings m_configPrecision; //!< Stores editor settings for the profiler + Configs::DebugSettings m_configDebug; //!< Stores editor settings for the profiler }; } // namespace FPSProfiler From 46cff253aa081fe88952ff73a30b5d8fa5ac326f Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 10:39:32 +0100 Subject: [PATCH 105/175] change version to 0 | use static cast Signed-off-by: Wojciech Czerski --- .../Code/Source/Tools/FPSProfilerConfig.cpp | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index fe79b0ef..11b38a4f 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -128,14 +128,14 @@ namespace FPSProfiler::Configs AZ::Edit::Attributes::ButtonText, [](void* instance) -> AZStd::string { - auto* self = reinterpret_cast(instance); + auto* self = static_cast(instance); return (self->m_RecordStats & RecordStatistics::FPS) ? "Disable FPS" : "Enable FPS"; }) ->Attribute( AZ::Edit::Attributes::ChangeNotify, [](void* instance) { - auto* self = reinterpret_cast(instance); + auto* self = static_cast(instance); self->m_RecordStats = static_cast(self->m_RecordStats ^ RecordStatistics::FPS); // Toggle bit return AZ::Edit::PropertyRefreshLevels::ValuesOnly; }) @@ -146,14 +146,14 @@ namespace FPSProfiler::Configs AZ::Edit::Attributes::ButtonText, [](void* instance) -> AZStd::string { - auto* self = reinterpret_cast(instance); + auto* self = static_cast(instance); return (self->m_RecordStats & RecordStatistics::CPU) ? "Disable CPU" : "Enable CPU"; }) ->Attribute( AZ::Edit::Attributes::ChangeNotify, [](void* instance) { - auto* self = reinterpret_cast(instance); + auto* self = static_cast(instance); self->m_RecordStats = static_cast(self->m_RecordStats ^ RecordStatistics::CPU); // Toggle bit return AZ::Edit::PropertyRefreshLevels::ValuesOnly; }) @@ -164,14 +164,14 @@ namespace FPSProfiler::Configs AZ::Edit::Attributes::ButtonText, [](void* instance) -> AZStd::string { - auto* self = reinterpret_cast(instance); + auto* self = static_cast(instance); return (self->m_RecordStats & RecordStatistics::GPU) ? "Disable GPU" : "Enable GPU"; }) ->Attribute( AZ::Edit::Attributes::ChangeNotify, [](void* instance) { - auto* self = reinterpret_cast(instance); + auto* self = static_cast(instance); self->m_RecordStats = static_cast(self->m_RecordStats ^ RecordStatistics::GPU); // Toggle bit return AZ::Edit::PropertyRefreshLevels::ValuesOnly; }); @@ -188,10 +188,10 @@ namespace FPSProfiler::Configs ->Value("Exponential", MovingAverageType::Exponential); serializeContext->Class() - ->Version(1) - ->Field("NearZeroPrecision", &PrecisionSettings::m_NearZeroPrecision) - ->Field("AverageFpsType", &PrecisionSettings::m_avgFpsType) - ->Field("UseAverageMedianFilter", &PrecisionSettings::m_useAvgMedianFilter); + ->Version(0) + ->Field("m_NearZeroPrecision", &PrecisionSettings::m_NearZeroPrecision) + ->Field("m_avgFpsType", &PrecisionSettings::m_avgFpsType) + ->Field("m_useAvgMedianFilter", &PrecisionSettings::m_useAvgMedianFilter); if (auto* editContext = serializeContext->GetEditContext()) { @@ -222,7 +222,7 @@ namespace FPSProfiler::Configs if (auto* serializeContext = azrtti_cast(context)) { serializeContext->Class() - ->Version(1) + ->Version(0) ->Field("PrintDebugInfo", &DebugSettings::m_PrintDebugInfo) ->Field("ShowFps", &DebugSettings::m_ShowFps) ->Field("Color", &DebugSettings::m_Color); From 7ef05560c77565d114d5edb84991ade9f737996e Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 10:51:25 +0100 Subject: [PATCH 106/175] remove reflect duplicates Signed-off-by: Wojciech Czerski --- .../Code/Source/Tools/FPSProfilerConfig.cpp | 20 ++++--------------- .../Code/Source/Tools/FPSProfilerConfig.h | 4 ++-- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index 11b38a4f..3410ecef 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -99,28 +99,16 @@ namespace FPSProfiler::Configs return data && data->m_recordType == RecordType::FramePick ? AZ::Edit::PropertyVisibility::Show : AZ::Edit::PropertyVisibility::Hide; }) - - ->DataElement( - AZ::Edit::UIHandlers::Default, - &RecordSettings::m_recordType, - "Record Type", - "Specifies the type of desired record.") - - ->DataElement( - AZ::Edit::UIHandlers::Default, - &RecordSettings::m_framesToSkip, - "Frames To Skip", - "Number of frames to skip before starting recording.") - ->Attribute(AZ::Edit::Attributes::Min, 0.0f) - ->Attribute(AZ::Edit::Attributes::Step, 1.0f) + ->Attribute(AZ::Edit::Attributes::Min, 0.) + ->Attribute(AZ::Edit::Attributes::Step, 1) ->DataElement( AZ::Edit::UIHandlers::Default, &RecordSettings::m_framesToRecord, "Frames To Record", "Number of frames to capture. If set to 0.0f it will be skipped.") - ->Attribute(AZ::Edit::Attributes::Min, 0.0f) - ->Attribute(AZ::Edit::Attributes::Step, 100.0f) + ->Attribute(AZ::Edit::Attributes::Min, 0.) + ->Attribute(AZ::Edit::Attributes::Step, 100) // FPS Button ->UIElement(AZ::Edit::UIHandlers::Button, "Toggle Save FPS", "Toggle FPS recording") diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h index c7adf349..1b35f6f3 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h @@ -15,7 +15,7 @@ namespace FPSProfiler::Configs }; AZ_DEFINE_ENUM_RELATIONAL_OPERATORS(MovingAverageType); - enum RecordType + enum RecordType : uint8_t { GameStart = 0, FramePick = 1, @@ -53,7 +53,7 @@ namespace FPSProfiler::Configs RecordType m_recordType = RecordType::GameStart; int m_framesToSkip = 0; // Available only for FramePick - float m_framesToRecord = 0.0f; + int m_framesToRecord = 0; RecordStatistics m_RecordStats = RecordStatistics::All; }; From 502c8bb9beb72a3dce7ab0bda907527a7065d618 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 10:52:55 +0100 Subject: [PATCH 107/175] fix tick comparison Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 25d2e427..841126fe 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -122,7 +122,7 @@ namespace FPSProfiler return; } - if (m_configRecord.m_framesToRecord <= m_configPrecision.m_NearZeroPrecision) + if (m_configRecord.m_framesToRecord != 0) { static int localFrameCount = 0; if (m_configRecord.m_framesToRecord == localFrameCount) From eb527dc735cd58e40d026b97ba7f3fdce611951b Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 11:01:56 +0100 Subject: [PATCH 108/175] remove bool from enum Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp | 6 +++--- Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index 3410ecef..f060c67b 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -192,10 +192,10 @@ namespace FPSProfiler::Configs ->DataElement( AZ::Edit::UIHandlers::ComboBox, &PrecisionSettings::m_avgFpsType, - "Average FPS Type", + "Moving Average Type", "Select the type of moving average to use") - ->EnumAttribute(MovingAverageType::Simple, "Simple Moving Average") - ->EnumAttribute(MovingAverageType::Exponential, "Exponential Moving Average") + ->EnumAttribute(MovingAverageType::Simple, "Simple") + ->EnumAttribute(MovingAverageType::Exponential, "Exponential") ->DataElement( AZ::Edit::UIHandlers::CheckBox, &PrecisionSettings::m_useAvgMedianFilter, diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h index 1b35f6f3..3fc2e9b3 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h @@ -8,7 +8,7 @@ namespace FPSProfiler::Configs { - enum MovingAverageType : bool + enum MovingAverageType { Simple = 0, Exponential = 1, From fc4f2d9740a45fb89e2432a30412a51076916625 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 11:13:09 +0100 Subject: [PATCH 109/175] add EMA calculation | add smoothing factor Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 20 ++++++++++++++++++- .../Code/Source/Tools/FPSProfilerConfig.cpp | 20 +++++++++++++++++-- .../Code/Source/Tools/FPSProfilerConfig.h | 3 ++- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 841126fe..582605ef 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -366,7 +366,25 @@ namespace FPSProfiler m_fpsSamples.pop_front(); } - m_avgFps = AZStd::accumulate(m_fpsSamples.begin(), m_fpsSamples.end(), 0.0f) / static_cast(m_fpsSamples.size()); + if (m_configPrecision.m_avgFpsType == Configs::MovingAverageType::Simple) + { + m_avgFps = AZStd::accumulate(m_fpsSamples.begin(), m_fpsSamples.end(), 0.0f) / static_cast(m_fpsSamples.size()); + } + else + { + const float alpha = m_configPrecision.m_smoothingFactor / (m_configFile.m_AutoSaveAtFrame + 1); // Smoothing factor + // Compute EMA + if (m_fpsSamples.size() == 1) + { + // Initialize EMA with the first FPS value + m_avgFps = m_currentFps; + } + else + { + // Apply EMA formula + m_avgFps = (alpha * m_currentFps) + ((1.0f - alpha) * m_avgFps); + } + } // Using m_NearZeroPrecision, since m_currentFPS cannot be equal to 0 if delta time is valid. if (m_currentFps >= m_configPrecision.m_NearZeroPrecision) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index f060c67b..008d66f8 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -99,7 +99,7 @@ namespace FPSProfiler::Configs return data && data->m_recordType == RecordType::FramePick ? AZ::Edit::PropertyVisibility::Show : AZ::Edit::PropertyVisibility::Hide; }) - ->Attribute(AZ::Edit::Attributes::Min, 0.) + ->Attribute(AZ::Edit::Attributes::Min, 0) ->Attribute(AZ::Edit::Attributes::Step, 1) ->DataElement( @@ -107,7 +107,7 @@ namespace FPSProfiler::Configs &RecordSettings::m_framesToRecord, "Frames To Record", "Number of frames to capture. If set to 0.0f it will be skipped.") - ->Attribute(AZ::Edit::Attributes::Min, 0.) + ->Attribute(AZ::Edit::Attributes::Min, 0) ->Attribute(AZ::Edit::Attributes::Step, 100) // FPS Button @@ -179,6 +179,7 @@ namespace FPSProfiler::Configs ->Version(0) ->Field("m_NearZeroPrecision", &PrecisionSettings::m_NearZeroPrecision) ->Field("m_avgFpsType", &PrecisionSettings::m_avgFpsType) + ->Field("m_smoothingFactor", &PrecisionSettings::m_smoothingFactor) ->Field("m_useAvgMedianFilter", &PrecisionSettings::m_useAvgMedianFilter); if (auto* editContext = serializeContext->GetEditContext()) @@ -196,6 +197,21 @@ namespace FPSProfiler::Configs "Select the type of moving average to use") ->EnumAttribute(MovingAverageType::Simple, "Simple") ->EnumAttribute(MovingAverageType::Exponential, "Exponential") + ->DataElement( + AZ::Edit::UIHandlers::Default, + &PrecisionSettings::m_smoothingFactor, + "Alpha Smoothing Factor", + "Alpha Smoothing Factor for Exponential Average Calculation.") + ->Attribute(AZ::Edit::Attributes::Min, 0) + ->Attribute(AZ::Edit::Attributes::Step, 0.1) + ->Attribute( + AZ::Edit::Attributes::Visibility, + [](const void* instance) + { + const PrecisionSettings* data = static_cast(instance); + return data && data->m_avgFpsType == Exponential ? AZ::Edit::PropertyVisibility::Show + : AZ::Edit::PropertyVisibility::Hide; + }) ->DataElement( AZ::Edit::UIHandlers::CheckBox, &PrecisionSettings::m_useAvgMedianFilter, diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h index 3fc2e9b3..a001158e 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h @@ -63,7 +63,8 @@ namespace FPSProfiler::Configs static void Reflect(AZ::ReflectContext* context); float m_NearZeroPrecision = 0.01f; - MovingAverageType m_avgFpsType = MovingAverageType::Exponential; + MovingAverageType m_avgFpsType = MovingAverageType::Simple; + float m_smoothingFactor = 2.0f; bool m_useAvgMedianFilter = true; }; From 158f1f79a2d60e0be8bbf988f9a7009bd6b39be3 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 11:16:19 +0100 Subject: [PATCH 110/175] refresh ui Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index 008d66f8..a42a61a3 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -197,6 +197,7 @@ namespace FPSProfiler::Configs "Select the type of moving average to use") ->EnumAttribute(MovingAverageType::Simple, "Simple") ->EnumAttribute(MovingAverageType::Exponential, "Exponential") + ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree) ->DataElement( AZ::Edit::UIHandlers::Default, &PrecisionSettings::m_smoothingFactor, From eeacd8176521d3145114d4f432e980965a9ac4c6 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 11:23:29 +0100 Subject: [PATCH 111/175] enable print when debug selected Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 24 ++++++++++++------- .../Clients/FPSProfilerSystemComponent.h | 2 +- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 582605ef..4ecddeb9 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -69,7 +69,11 @@ namespace FPSProfiler if (!IsPathValid(m_configFile.m_OutputFilename)) { m_configFile.m_OutputFilename = "@user@/fps_log.csv"; - AZ_Warning("FPSProfiler", false, "Invalid output file path. Using default: %s", m_configFile.m_OutputFilename.c_str()); + AZ_Warning( + "FPSProfiler", + !m_configDebug.m_PrintDebugInfo, + "Invalid output file path. Using default: %s", + m_configFile.m_OutputFilename.c_str()); } FPSProfilerRequestBus::Handler::BusConnect(); // connect first to broadcast notifications @@ -198,7 +202,7 @@ namespace FPSProfiler { if (m_isProfiling) { - AZ_Warning("FPS Profiler", false, "Profiler already activated."); + AZ_Warning("FPS Profiler", !m_configDebug.m_PrintDebugInfo, "Profiler already activated."); return; } @@ -221,7 +225,7 @@ namespace FPSProfiler { if (!m_isProfiling) { - AZ_Warning("FPS Profiler", false, "Profiler already stopped."); + AZ_Warning("FPS Profiler", !m_configDebug.m_PrintDebugInfo, "Profiler already stopped."); return; } @@ -273,7 +277,7 @@ namespace FPSProfiler } m_configFile.m_OutputFilename = newSavePath; - AZ_Warning("FPS Profiler", !m_isProfiling, "Path changed during activated profiling."); + AZ_Warning("FPS Profiler", !m_configDebug.m_PrintDebugInfo && !m_isProfiling, "Path changed during activated profiling."); } void FPSProfilerSystemComponent::SafeChangeSavePath(const AZ::IO::Path& newSavePath) @@ -399,14 +403,18 @@ namespace FPSProfiler { if (!IsAnySaveOptionEnabled()) { - AZ_Warning("FPSProfiler", false, "None save option selected. Skipping file creation."); + AZ_Warning("FPSProfiler", !m_configDebug.m_PrintDebugInfo, "None save option selected. Skipping file creation."); return; } if (!IsPathValid(m_configFile.m_OutputFilename)) { m_configFile.m_OutputFilename = "@user@/fps_log.csv"; - AZ_Warning("FPSProfiler", false, "Invalid output file path. Using default: %s", m_configFile.m_OutputFilename.c_str()); + AZ_Warning( + "FPSProfiler", + !m_configDebug.m_PrintDebugInfo, + "Invalid output file path. Using default: %s", + m_configFile.m_OutputFilename.c_str()); } // Apply Timestamp @@ -467,7 +475,7 @@ namespace FPSProfiler return static_cast(bytes) / (1024.0f * 1024.0f); } - bool FPSProfilerSystemComponent::IsPathValid(const AZ::IO::Path& path) + bool FPSProfilerSystemComponent::IsPathValid(const AZ::IO::Path& path) const { AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); @@ -479,7 +487,7 @@ namespace FPSProfiler : !fileIO ? "Could not get a FileIO object. Try again." : "Path is not registered or recognizable by O3DE FileIO System."; - AZ_Warning("FPSProfiler::ChangeSavePath", false, "%s", reason); + AZ_Warning("FPSProfiler::ChangeSavePath", !m_configDebug.m_PrintDebugInfo, "%s", reason); return false; } diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 5514b06b..8d3dd6e1 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -89,7 +89,7 @@ namespace FPSProfiler // Utility Functions void CalculateFpsData(const float& deltaTime); static float BytesToMB(AZStd::size_t bytes); - static bool IsPathValid(const AZ::IO::Path& path); + bool IsPathValid(const AZ::IO::Path& path) const; // Debug Display void ShowFps() const; From 543c3d841d7c9ad58f90bc7baa5af035ab266d2c Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 11:29:45 +0100 Subject: [PATCH 112/175] reflect enum Signed-off-by: Wojciech Czerski --- .../Code/Source/Tools/FPSProfilerConfig.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index a42a61a3..80f4bc63 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -62,6 +62,18 @@ namespace FPSProfiler::Configs { if (auto serializeContext = azrtti_cast(context)) { + serializeContext->Enum() + ->Value("GameStart", RecordType::GameStart) + ->Value("FramePick", RecordType::FramePick) + ->Value("Await", RecordType::Await); + + serializeContext->Enum() + ->Value("None", RecordStatistics::None) + ->Value("FPS", RecordStatistics::FPS) + ->Value("CPU", RecordStatistics::CPU) + ->Value("GPU", RecordStatistics::GPU) + ->Value("MemoryUsage", RecordStatistics::MemoryUsage); + serializeContext->Class() ->Version(0) ->Field("m_recordType", &RecordSettings::m_recordType) From 79e231bb665caf3af298a06d4d9f851fd4ff6bde Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 11:48:40 +0100 Subject: [PATCH 113/175] remove bool toogles | using enum values Signed-off-by: Wojciech Czerski --- .../Code/Source/Tools/FPSProfilerConfig.cpp | 81 +++---------------- 1 file changed, 12 insertions(+), 69 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index 80f4bc63..ffd90bd2 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -62,18 +62,6 @@ namespace FPSProfiler::Configs { if (auto serializeContext = azrtti_cast(context)) { - serializeContext->Enum() - ->Value("GameStart", RecordType::GameStart) - ->Value("FramePick", RecordType::FramePick) - ->Value("Await", RecordType::Await); - - serializeContext->Enum() - ->Value("None", RecordStatistics::None) - ->Value("FPS", RecordStatistics::FPS) - ->Value("CPU", RecordStatistics::CPU) - ->Value("GPU", RecordStatistics::GPU) - ->Value("MemoryUsage", RecordStatistics::MemoryUsage); - serializeContext->Class() ->Version(0) ->Field("m_recordType", &RecordSettings::m_recordType) @@ -87,17 +75,13 @@ namespace FPSProfiler::Configs ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) - // Reflect enum with ComboBox: ->DataElement( AZ::Edit::UIHandlers::ComboBox, &RecordSettings::m_recordType, "Record Type", "Specifies the type of record.") - // Provide the combo‐box choices: ->EnumAttribute(RecordType::GameStart, "Game Start") ->EnumAttribute(RecordType::FramePick, "Frame Pick") ->EnumAttribute(RecordType::Await, "Await") - // Ensure the UI updates when the enum changes: ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree) - // Conditionally show m_framesToSkip only if "Frame Pick" is selected: ->DataElement( AZ::Edit::UIHandlers::Default, &RecordSettings::m_framesToSkip, @@ -122,59 +106,18 @@ namespace FPSProfiler::Configs ->Attribute(AZ::Edit::Attributes::Min, 0) ->Attribute(AZ::Edit::Attributes::Step, 100) - // FPS Button - ->UIElement(AZ::Edit::UIHandlers::Button, "Toggle Save FPS", "Toggle FPS recording") - ->Attribute( - AZ::Edit::Attributes::ButtonText, - [](void* instance) -> AZStd::string - { - auto* self = static_cast(instance); - return (self->m_RecordStats & RecordStatistics::FPS) ? "Disable FPS" : "Enable FPS"; - }) - ->Attribute( - AZ::Edit::Attributes::ChangeNotify, - [](void* instance) - { - auto* self = static_cast(instance); - self->m_RecordStats = static_cast(self->m_RecordStats ^ RecordStatistics::FPS); // Toggle bit - return AZ::Edit::PropertyRefreshLevels::ValuesOnly; - }) - - // CPU Button - ->UIElement(AZ::Edit::UIHandlers::Button, "Toggle Save CPU", "Toggle CPU recording") - ->Attribute( - AZ::Edit::Attributes::ButtonText, - [](void* instance) -> AZStd::string - { - auto* self = static_cast(instance); - return (self->m_RecordStats & RecordStatistics::CPU) ? "Disable CPU" : "Enable CPU"; - }) - ->Attribute( - AZ::Edit::Attributes::ChangeNotify, - [](void* instance) - { - auto* self = static_cast(instance); - self->m_RecordStats = static_cast(self->m_RecordStats ^ RecordStatistics::CPU); // Toggle bit - return AZ::Edit::PropertyRefreshLevels::ValuesOnly; - }) - - // GPU Button - ->UIElement(AZ::Edit::UIHandlers::Button, "Save Save GPU", "Toggle GPU recording") - ->Attribute( - AZ::Edit::Attributes::ButtonText, - [](void* instance) -> AZStd::string - { - auto* self = static_cast(instance); - return (self->m_RecordStats & RecordStatistics::GPU) ? "Disable GPU" : "Enable GPU"; - }) - ->Attribute( - AZ::Edit::Attributes::ChangeNotify, - [](void* instance) - { - auto* self = static_cast(instance); - self->m_RecordStats = static_cast(self->m_RecordStats ^ RecordStatistics::GPU); // Toggle bit - return AZ::Edit::PropertyRefreshLevels::ValuesOnly; - }); + ->DataElement( + AZ::Edit::UIHandlers::ComboBox, + &RecordSettings::m_RecordStats, + "Record Stats", + "Specifies the type of stats that will be saved to file.") + ->EnumAttribute(RecordStatistics::None, "None") + ->EnumAttribute(RecordStatistics::FPS, "FPS") + ->EnumAttribute(RecordStatistics::CPU, "CPU") + ->EnumAttribute(RecordStatistics::GPU, "GPU") + ->EnumAttribute(RecordStatistics::MemoryUsage, "MemoryUsage") + ->EnumAttribute(RecordStatistics::All, "All") + ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree); } } } From f809190b1a5cfbb52f73202fd985ebd8407eaabe Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 11:51:44 +0100 Subject: [PATCH 114/175] fix mask comparison Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 4ecddeb9..654d721b 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -150,21 +150,21 @@ namespace FPSProfiler float usedGpu = -1.0f, reservedGpu = -1.0f; const char* logEntryFormat = "-1,-1.0,-1.0,-1.0,-1.0,-1.0,%.2f,%.2f,%.2f,%.2f\n"; - if (m_configRecord.m_RecordStats | Configs::RecordStatistics::CPU) + if (m_configRecord.m_RecordStats & Configs::RecordStatistics::CPU) { auto [cpuUsed, cpuReserved] = GetCpuMemoryUsed(); usedCpu = BytesToMB(cpuUsed); reservedCpu = BytesToMB(cpuReserved); } - if (m_configRecord.m_RecordStats | Configs::RecordStatistics::GPU) + if (m_configRecord.m_RecordStats & Configs::RecordStatistics::GPU) { auto [gpuUsed, gpuReserved] = GetGpuMemoryUsed(); usedGpu = BytesToMB(gpuUsed); reservedGpu = BytesToMB(gpuReserved); } - if (m_configRecord.m_RecordStats | Configs::RecordStatistics::FPS) + if (m_configRecord.m_RecordStats & Configs::RecordStatistics::FPS) { logEntryFormat = "%d,%.4f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n"; } @@ -266,7 +266,7 @@ namespace FPSProfiler bool FPSProfilerSystemComponent::IsAnySaveOptionEnabled() const { - return !(m_configRecord.m_RecordStats | Configs::RecordStatistics::None); + return m_configRecord.m_RecordStats != Configs::RecordStatistics::None; } void FPSProfilerSystemComponent::ChangeSavePath(const AZ::IO::Path& newSavePath) From 1caa08a80f1536cf4b6c70f14ec17d7e31a6646b Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 12:28:54 +0100 Subject: [PATCH 115/175] typo fix Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 654d721b..a7f0ca9d 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -363,7 +363,7 @@ namespace FPSProfiler m_totalFrameTime += deltaTime; m_frameCount++; - // Latest fps hisotry for avg fps calculation + // Latest fps history for avg fps calculation m_fpsSamples.push_back(m_currentFps); if (m_fpsSamples.size() > m_configFile.m_AutoSaveAtFrame) { From 23fba4efdf94608efe23da48536e12cba4ab5ed8 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 12:37:36 +0100 Subject: [PATCH 116/175] fix save data on exit Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index a7f0ca9d..869e42c6 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -98,7 +98,10 @@ namespace FPSProfiler void FPSProfilerSystemComponent::Deactivate() { AZ::TickBus::Handler::BusDisconnect(); - WriteDataToFile(); + if (m_configRecord.m_framesToRecord == 0) + { + WriteDataToFile(); + } // Notify - File Saved FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileSaved, m_configFile.m_OutputFilename.c_str()); From 9be7ec016ffd0acfd43d05478b52f087606d829b Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 13:02:55 +0100 Subject: [PATCH 117/175] fix prfoile start Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 869e42c6..a719adeb 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -76,14 +76,14 @@ namespace FPSProfiler m_configFile.m_OutputFilename.c_str()); } - FPSProfilerRequestBus::Handler::BusConnect(); // connect first to broadcast notifications - AZ::TickBus::Handler::BusConnect(); // connect last, after setup - // Reserve log entries buffer size based on known auto save per frame m_configFile.m_AutoSave ? m_logBuffer.reserve(MAX_LOG_BUFFER_LINE_SIZE * m_configFile.m_AutoSaveAtFrame * 2) : m_logBuffer.reserve(MAX_LOG_BUFFER_SIZE); - if (m_configRecord.m_recordType == Configs::RecordType::GameStart) + FPSProfilerRequestBus::Handler::BusConnect(); // connect first to broadcast notifications + AZ::TickBus::Handler::BusConnect(); // connect last, after setup + + if (m_configRecord.m_recordType != Configs::RecordType::Await) { AZ::TickBus::QueueFunction( [this]() @@ -123,7 +123,7 @@ namespace FPSProfiler ShowFps(); } - if (m_configRecord.m_recordType == Configs::RecordType::FramePick && m_configRecord.m_framesToSkip > m_frameCount) + if (m_configRecord.m_recordType == Configs::RecordType::FramePick && m_frameCount < m_configRecord.m_framesToSkip) { // Wait for selected frame return; From e115b8d38f5361924a412b16f5993750d676413d Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 13:14:07 +0100 Subject: [PATCH 118/175] deactivate fix Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index a719adeb..b8acea99 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -98,7 +98,8 @@ namespace FPSProfiler void FPSProfilerSystemComponent::Deactivate() { AZ::TickBus::Handler::BusDisconnect(); - if (m_configRecord.m_framesToRecord == 0) + + if (!m_configFile.m_AutoSave || m_configRecord.m_framesToRecord == 0) { WriteDataToFile(); } @@ -123,7 +124,7 @@ namespace FPSProfiler ShowFps(); } - if (m_configRecord.m_recordType == Configs::RecordType::FramePick && m_frameCount < m_configRecord.m_framesToSkip) + if (m_configRecord.m_recordType == Configs::RecordType::FramePick && m_frameCount <= m_configRecord.m_framesToSkip) { // Wait for selected frame return; From 9f4a38608b4118945e8e200e222ec5c81e1d57a5 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 13:26:23 +0100 Subject: [PATCH 119/175] simplyfy frame count Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 5 ++--- .../Code/Source/Clients/FPSProfilerSystemComponent.h | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index b8acea99..60817af8 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -132,12 +132,10 @@ namespace FPSProfiler if (m_configRecord.m_framesToRecord != 0) { - static int localFrameCount = 0; - if (m_configRecord.m_framesToRecord == localFrameCount) + if (m_configRecord.m_framesToRecord == m_recordedFrameCount++) { StopProfiling(); } - localFrameCount++; } if (!IsAnySaveOptionEnabled()) @@ -255,6 +253,7 @@ namespace FPSProfiler m_currentFps = 0.0f; m_totalFrameTime = 0.0f; m_frameCount = 0; + m_recordedFrameCount = 0; m_fpsSamples.clear(); m_logBuffer.clear(); diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 8d3dd6e1..538ee0c1 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -74,6 +74,7 @@ namespace FPSProfiler float m_currentFps = 0.0f; //!< FPS in the current frame float m_totalFrameTime = 0.0f; //!< Time taken for the current frame int m_frameCount = 0; //!< Total number of frames processed + int m_recordedFrameCount = 0; //!< Total number of frames recorded. AZStd::deque m_fpsSamples; //!< Stores recent FPS values for averaging From 445ba4b04727524d6d3262881a111c059d0cd450 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 13:28:55 +0100 Subject: [PATCH 120/175] remove redundant if statement4 Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 60817af8..596a992e 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -130,12 +130,9 @@ namespace FPSProfiler return; } - if (m_configRecord.m_framesToRecord != 0) + if (m_configRecord.m_framesToRecord != 0 && m_configRecord.m_framesToRecord == m_recordedFrameCount++) { - if (m_configRecord.m_framesToRecord == m_recordedFrameCount++) - { - StopProfiling(); - } + StopProfiling(); } if (!IsAnySaveOptionEnabled()) From 3a1df1d42712b8f60afafab72a938c948283d029 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 13:34:34 +0100 Subject: [PATCH 121/175] apply seconds to timestamp Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 6 +++--- Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 596a992e..dbc2c12f 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -426,9 +426,9 @@ namespace FPSProfiler std::tm timeInfo{}; localtime_r(&now_time_t, &timeInfo); - // Format the timestamp as YYYYMMDD_HHMM - char timestamp[16]; - strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M", &timeInfo); + // Format the timestamp as YYYYMMDD_HHMMSS + char timestamp[20]; + strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", &timeInfo); m_configFile.m_OutputFilename.ReplaceFilename( (m_configFile.m_OutputFilename.Stem().String() + "_" + timestamp + m_configFile.m_OutputFilename.Extension().String()) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index ffd90bd2..916efdde 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -53,7 +53,7 @@ namespace FPSProfiler::Configs AZ::Edit::UIHandlers::Default, &FileSaveSettings::m_SaveWithTimestamp, "Timestamp", - "When enabled, system will save files with timestamp postfix of current date and hour."); + "When enabled, system will save files with timestamp postfix of current date, hour, minutes and seconds."); } } } From 9e70d787720f4435d59ac9b396f9444e9376ba06 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 13:38:55 +0100 Subject: [PATCH 122/175] fix comment Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 538ee0c1..7424ec38 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -74,7 +74,7 @@ namespace FPSProfiler float m_currentFps = 0.0f; //!< FPS in the current frame float m_totalFrameTime = 0.0f; //!< Time taken for the current frame int m_frameCount = 0; //!< Total number of frames processed - int m_recordedFrameCount = 0; //!< Total number of frames recorded. + int m_recordedFrameCount = 0; //!< Total number of frames recorded. Used when @ref m_configRecord.m_framesToRecord != 0 AZStd::deque m_fpsSamples; //!< Stores recent FPS values for averaging From f3dd0e9ebd71e4a3be23cf641e18c694bec7dcce Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 6 Mar 2025 14:26:07 +0100 Subject: [PATCH 123/175] fix file save on deactivation Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index dbc2c12f..b122f358 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -99,7 +99,7 @@ namespace FPSProfiler { AZ::TickBus::Handler::BusDisconnect(); - if (!m_configFile.m_AutoSave || m_configRecord.m_framesToRecord == 0) + if (!m_configFile.m_AutoSave || m_configRecord.m_framesToRecord == 0 || m_logBuffer.size() < m_configFile.m_AutoSaveAtFrame) { WriteDataToFile(); } From 02eea87585686e18feed70b1dfea16d698a2f420 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Fri, 7 Mar 2025 12:16:41 +0100 Subject: [PATCH 124/175] remove enum serialization Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp index 916efdde..12f3e6a9 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp @@ -126,10 +126,6 @@ namespace FPSProfiler::Configs { if (auto* serializeContext = azrtti_cast(context)) { - serializeContext->Enum() - ->Value("Simple", MovingAverageType::Simple) - ->Value("Exponential", MovingAverageType::Exponential); - serializeContext->Class() ->Version(0) ->Field("m_NearZeroPrecision", &PrecisionSettings::m_NearZeroPrecision) From 269ddc443d6c0e909bf95de5f57e5c0154a2eff6 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 11 Mar 2025 12:38:22 +0100 Subject: [PATCH 125/175] Add Config reflection to System Component Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index b122f358..0f4ae271 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -11,6 +11,11 @@ namespace FPSProfiler { void FPSProfilerSystemComponent::Reflect(AZ::ReflectContext* context) { + Configs::FileSaveSettings::Reflect(context); + Configs::RecordSettings::Reflect(context); + Configs::PrecisionSettings::Reflect(context); + Configs::DebugSettings::Reflect(context); + if (auto serializeContext = azrtti_cast(context)) { serializeContext->Class() From 36296d63ffab1d2438e1cc80299d4f460613d91e Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 11 Mar 2025 12:52:40 +0100 Subject: [PATCH 126/175] create config dir | move config | fix config reflect Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h | 2 +- .../Code/Source/Clients/FPSProfilerSystemComponent.h | 2 +- .../Source/{Tools => Configurations}/FPSProfilerConfig.cpp | 0 .../Code/Source/{Tools => Configurations}/FPSProfilerConfig.h | 0 .../Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp | 2 ++ .../Code/Source/Tools/FPSProfilerEditorSystemComponent.h | 3 +-- Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake | 2 -- Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake | 2 ++ 8 files changed, 7 insertions(+), 6 deletions(-) rename Gems/FPSProfiler/Code/Source/{Tools => Configurations}/FPSProfilerConfig.cpp (100%) rename Gems/FPSProfiler/Code/Source/{Tools => Configurations}/FPSProfilerConfig.h (100%) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 043edb28..fc773ce7 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -1,7 +1,7 @@ #pragma once +#include #include -#include #include #include diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index 7424ec38..fd867fc0 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -1,7 +1,7 @@ #pragma once +#include #include -#include #include #include diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp similarity index 100% rename from Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.cpp rename to Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.h similarity index 100% rename from Gems/FPSProfiler/Code/Source/Tools/FPSProfilerConfig.h rename to Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.h diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 6a2caae3..86f03efe 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -1,5 +1,7 @@ #include "FPSProfilerEditorSystemComponent.h" +#include + #include #include diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h index 46f048dd..9ec89297 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h @@ -1,7 +1,6 @@ #pragma once -#include "FPSProfilerConfig.h" -#include +#include #include #include diff --git a/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake index 716f2d65..934da04a 100644 --- a/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake +++ b/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake @@ -1,7 +1,5 @@ set(FILES - Source/Tools/FPSProfilerConfig.cpp - Source/Tools/FPSProfilerConfig.h Source/Tools/FPSProfilerEditorSystemComponent.cpp Source/Tools/FPSProfilerEditorSystemComponent.h ) diff --git a/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake index 92006e5d..b9e62b61 100644 --- a/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake +++ b/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake @@ -4,4 +4,6 @@ set(FILES Source/FPSProfilerModuleInterface.h Source/Clients/FPSProfilerSystemComponent.cpp Source/Clients/FPSProfilerSystemComponent.h + Source/Configurations/FPSProfilerConfig.cpp + Source/Configurations/FPSProfilerConfig.h ) From 8b0a5733e7eef01079d7eb02ec22dd29a2c89a4f Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 11 Mar 2025 13:08:35 +0100 Subject: [PATCH 127/175] simplify if check Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.cpp | 4 ++-- .../Code/Source/Clients/FPSProfilerSystemComponent.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index 0f4ae271..d77224c0 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -39,7 +39,7 @@ namespace FPSProfiler FPSProfilerSystemComponent::FPSProfilerSystemComponent() { - if (FPSProfilerInterface::Get() == nullptr) + if (!FPSProfilerInterface::Get()) { FPSProfilerInterface::Register(this); } @@ -55,7 +55,7 @@ namespace FPSProfiler , m_configPrecision(AZStd::move(configP)) , m_configDebug(AZStd::move(configD)) { - if (FPSProfilerInterface::Get() == nullptr) + if (!FPSProfilerInterface::Get()) { FPSProfilerInterface::Register(this); } diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index fd867fc0..f26af136 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -9,7 +9,7 @@ namespace FPSProfiler { - class FPSProfilerSystemComponent final + class FPSProfilerSystemComponent : public AZ::Component , protected FPSProfilerRequestBus::Handler , public AZ::TickBus::Handler @@ -90,7 +90,7 @@ namespace FPSProfiler // Utility Functions void CalculateFpsData(const float& deltaTime); static float BytesToMB(AZStd::size_t bytes); - bool IsPathValid(const AZ::IO::Path& path) const; + [[nodiscard]] bool IsPathValid(const AZ::IO::Path& path) const; // Debug Display void ShowFps() const; From bb3fd64c97ec9abeea1585181074a60a53a79519 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 11 Mar 2025 13:09:45 +0100 Subject: [PATCH 128/175] add final to system component Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerSystemComponent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index f26af136..d138497d 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -9,7 +9,7 @@ namespace FPSProfiler { - class FPSProfilerSystemComponent + class FPSProfilerSystemComponent final : public AZ::Component , protected FPSProfilerRequestBus::Handler , public AZ::TickBus::Handler From 83e92dbd8ec3438d4b56edc5be0b9934a49c896b Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 10:14:38 +0100 Subject: [PATCH 129/175] swap IO Path to String | add button to add path Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 12 ++++----- .../Clients/FPSProfilerSystemComponent.cpp | 18 ++++++------- .../Clients/FPSProfilerSystemComponent.h | 8 +++--- .../Source/Configurations/FPSProfilerConfig.h | 2 +- .../FPSProfilerEditorSystemComponent.cpp | 26 +++++++++++++++++++ .../Tools/FPSProfilerEditorSystemComponent.h | 2 ++ 6 files changed, 48 insertions(+), 20 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index fc773ce7..af9d12bb 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -52,14 +52,14 @@ namespace FPSProfiler * @warning This function is NOT runtime safe. Use @ref SafeChangeSavePath instead. * @param newSavePath The new file path where profiling data should be saved. */ - virtual void ChangeSavePath(const AZ::IO::Path& newSavePath) = 0; + virtual void ChangeSavePath(const AZStd::string& newSavePath) = 0; /** * @brief Safely changes the save path during runtime. * This method stops profiling, saves the current data, and then updates the path. * @param newSavePath The new file path where profiling data should be saved. */ - virtual void SafeChangeSavePath(const AZ::IO::Path& newSavePath) = 0; + virtual void SafeChangeSavePath(const AZStd::string& newSavePath) = 0; /** * @brief Retrieves the minimum recorded FPS during the profiling session. @@ -107,7 +107,7 @@ namespace FPSProfiler * @param newSavePath The csv file path where the log should be saved. * @param useSafeChangePath If true, the function will use @ref SafeChangeSavePath to ensure runtime safety. */ - virtual void SaveLogToFileWithNewPath(const AZ::IO::Path& newSavePath, bool useSafeChangePath) = 0; + virtual void SaveLogToFileWithNewPath(const AZStd::string& newSavePath, bool useSafeChangePath) = 0; /** * @brief Enables or disables FPS display on-screen. @@ -140,7 +140,7 @@ namespace FPSProfiler * @brief Called when a new file is created. * @param filePath The path of the newly created file. */ - virtual void OnFileCreated(const AZ::IO::Path& filePath) + virtual void OnFileCreated(const AZStd::string& filePath) { } @@ -148,7 +148,7 @@ namespace FPSProfiler * @brief Called when an existing file is updated. * @param filePath The path of the file that was modified. */ - virtual void OnFileUpdate(const AZ::IO::Path& filePath) + virtual void OnFileUpdate(const AZStd::string& filePath) { } @@ -156,7 +156,7 @@ namespace FPSProfiler * @brief Called when a file is successfully saved. * @param filePath The path of the saved file. */ - virtual void OnFileSaved(const AZ::IO::Path& filePath) + virtual void OnFileSaved(const AZStd::string& filePath) { } diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index d77224c0..d7ce54be 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -274,7 +274,7 @@ namespace FPSProfiler return m_configRecord.m_RecordStats != Configs::RecordStatistics::None; } - void FPSProfilerSystemComponent::ChangeSavePath(const AZ::IO::Path& newSavePath) + void FPSProfilerSystemComponent::ChangeSavePath(const AZStd::string& newSavePath) { if (!IsPathValid(newSavePath)) { @@ -285,7 +285,7 @@ namespace FPSProfiler AZ_Warning("FPS Profiler", !m_configDebug.m_PrintDebugInfo && !m_isProfiling, "Path changed during activated profiling."); } - void FPSProfilerSystemComponent::SafeChangeSavePath(const AZ::IO::Path& newSavePath) + void FPSProfilerSystemComponent::SafeChangeSavePath(const AZStd::string& newSavePath) { // If profiling is enabled, save current opened file and stop profiling. StopProfiling(); @@ -343,7 +343,7 @@ namespace FPSProfiler WriteDataToFile(); } - void FPSProfilerSystemComponent::SaveLogToFileWithNewPath(const AZ::IO::Path& newSavePath, bool useSafeChangePath) + void FPSProfilerSystemComponent::SaveLogToFileWithNewPath(const AZStd::string& newSavePath, bool useSafeChangePath) { if (useSafeChangePath) { @@ -435,8 +435,8 @@ namespace FPSProfiler char timestamp[20]; strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", &timeInfo); - m_configFile.m_OutputFilename.ReplaceFilename( - (m_configFile.m_OutputFilename.Stem().String() + "_" + timestamp + m_configFile.m_OutputFilename.Extension().String()) + static_cast(m_configFile.m_OutputFilename).ReplaceFilename( + (static_cast(m_configFile.m_OutputFilename).Stem().String() + "_" + timestamp + static_cast(m_configFile.m_OutputFilename).Extension().String()) .data()); } @@ -480,15 +480,15 @@ namespace FPSProfiler return static_cast(bytes) / (1024.0f * 1024.0f); } - bool FPSProfilerSystemComponent::IsPathValid(const AZ::IO::Path& path) const + bool FPSProfilerSystemComponent::IsPathValid(const AZStd::string& path) const { AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); - if (path.empty() || !path.HasFilename() || !path.HasExtension() || !fileIO || !fileIO->ResolvePath(path.c_str())) + if (path.empty() || !static_cast(path).HasFilename() || !static_cast(path).HasExtension() || !fileIO || !fileIO->ResolvePath(path.c_str())) { const char* reason = path.empty() ? "Path cannot be empty." - : !path.HasFilename() ? "Path must have a file at the end." - : !path.HasExtension() ? "Path must have a *.csv extension." + : !static_cast(path).HasFilename() ? "Path must have a file at the end." + : !static_cast(path).HasExtension() ? "Path must have a *.csv extension." : !fileIO ? "Could not get a FileIO object. Try again." : "Path is not registered or recognizable by O3DE FileIO System."; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h index d138497d..ac5eb9f1 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h @@ -45,8 +45,8 @@ namespace FPSProfiler void ResetProfilingData() override; [[nodiscard]] bool IsProfiling() const override; [[nodiscard]] bool IsAnySaveOptionEnabled() const override; - void ChangeSavePath(const AZ::IO::Path& newSavePath) override; - void SafeChangeSavePath(const AZ::IO::Path& newSavePath) override; + void ChangeSavePath(const AZStd::string& newSavePath) override; + void SafeChangeSavePath(const AZStd::string& newSavePath) override; [[nodiscard]] float GetMinFps() const override; [[nodiscard]] float GetMaxFps() const override; [[nodiscard]] float GetAvgFps() const override; @@ -54,7 +54,7 @@ namespace FPSProfiler [[nodiscard]] AZStd::pair GetCpuMemoryUsed() const override; [[nodiscard]] AZStd::pair GetGpuMemoryUsed() const override; void SaveLogToFile() override; - void SaveLogToFileWithNewPath(const AZ::IO::Path& newSavePath, bool useSafeChangePath) override; + void SaveLogToFileWithNewPath(const AZStd::string& newSavePath, bool useSafeChangePath) override; void ShowFpsOnScreen(bool enable) override; private: @@ -90,7 +90,7 @@ namespace FPSProfiler // Utility Functions void CalculateFpsData(const float& deltaTime); static float BytesToMB(AZStd::size_t bytes); - [[nodiscard]] bool IsPathValid(const AZ::IO::Path& path) const; + [[nodiscard]] bool IsPathValid(const AZStd::string& path) const; // Debug Display void ShowFps() const; diff --git a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.h index a001158e..e1078d24 100644 --- a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.h +++ b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.h @@ -40,7 +40,7 @@ namespace FPSProfiler::Configs AZ_TYPE_INFO(FileSaveSettings, FPSProfilerConfigFileTypeId); static void Reflect(AZ::ReflectContext* context); - AZ::IO::Path m_OutputFilename = "@user@/fps_log.csv"; + AZStd::string m_OutputFilename = "@user@/fps_log.csv"; bool m_AutoSave = true; int m_AutoSaveAtFrame = 100; bool m_SaveWithTimestamp = true; diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 86f03efe..2f063e61 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -1,5 +1,8 @@ #include "FPSProfilerEditorSystemComponent.h" +#include "AzQtComponents/Components/Widgets/FileDialog.h" +#include "UI/UICore/WidgetHelpers.h" + #include #include @@ -31,6 +34,9 @@ namespace FPSProfiler ->Attribute(AZ::Edit::Attributes::Category, "Performance") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ->UIElement(AZ::Edit::UIHandlers::Button, "", "") + ->Attribute(AZ::Edit::Attributes::ButtonText, "Pick or Create a CSV") + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &FPSProfilerEditorSystemComponent::PickOrCreateCsvFile) ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configFile) ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configRecord) ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configPrecision) @@ -63,4 +69,24 @@ namespace FPSProfiler { entity->CreateComponent(m_configFile, m_configRecord, m_configPrecision, m_configDebug); } + + AZ::u32 FPSProfilerEditorSystemComponent::PickOrCreateCsvFile() + { + QString fileName = QFileDialog::getSaveFileName(AzToolsFramework::GetActiveWindow(), "Save CSV File", "", "Saves CSV Files (*.csv)"); + + if (fileName.isEmpty()) + { + QMessageBox::warning(AzToolsFramework::GetActiveWindow(), "Error", "Please specify file", QMessageBox::Ok); + return AZ::Edit::PropertyRefreshLevels::None; + } + + // Ensure the file has the .csv extension + if (!fileName.endsWith(".csv", Qt::CaseInsensitive)) + { + fileName += ".csv"; // Auto-append .csv if missing + } + + m_configFile.m_OutputFilename = AZStd::string(fileName.toUtf8().constData()); + return AZ::Edit::PropertyRefreshLevels::EntireTree; + } } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h index 9ec89297..fbd64fe8 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h @@ -33,5 +33,7 @@ namespace FPSProfiler Configs::RecordSettings m_configRecord; //!< Stores editor settings for the profiler Configs::PrecisionSettings m_configPrecision; //!< Stores editor settings for the profiler Configs::DebugSettings m_configDebug; //!< Stores editor settings for the profiler + + AZ::u32 PickOrCreateCsvFile(); }; } // namespace FPSProfiler From 5f5f80e65bf3e3f7511c60ae76da5ef18c3000bd Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 10:15:59 +0100 Subject: [PATCH 130/175] fix headers Signed-off-by: Wojciech Czerski --- .../Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 2f063e61..068ba1e5 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -1,12 +1,10 @@ #include "FPSProfilerEditorSystemComponent.h" - -#include "AzQtComponents/Components/Widgets/FileDialog.h" -#include "UI/UICore/WidgetHelpers.h" - #include #include #include +#include +#include namespace FPSProfiler { From 8f6c577a32c7b0bb6d383a0ec42c6001c709954f Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 10:18:35 +0100 Subject: [PATCH 131/175] rename function Signed-off-by: Wojciech Czerski --- .../Source/Tools/FPSProfilerEditorSystemComponent.cpp | 8 ++++---- .../Code/Source/Tools/FPSProfilerEditorSystemComponent.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 068ba1e5..95b91306 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -34,7 +34,7 @@ namespace FPSProfiler ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->UIElement(AZ::Edit::UIHandlers::Button, "", "") ->Attribute(AZ::Edit::Attributes::ButtonText, "Pick or Create a CSV") - ->Attribute(AZ::Edit::Attributes::ChangeNotify, &FPSProfilerEditorSystemComponent::PickOrCreateCsvFile) + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &FPSProfilerEditorSystemComponent::SelectCsvPath) ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configFile) ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configRecord) ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configPrecision) @@ -68,13 +68,13 @@ namespace FPSProfiler entity->CreateComponent(m_configFile, m_configRecord, m_configPrecision, m_configDebug); } - AZ::u32 FPSProfilerEditorSystemComponent::PickOrCreateCsvFile() + AZ::u32 FPSProfilerEditorSystemComponent::SelectCsvPath() { - QString fileName = QFileDialog::getSaveFileName(AzToolsFramework::GetActiveWindow(), "Save CSV File", "", "Saves CSV Files (*.csv)"); + QString fileName = QFileDialog::getSaveFileName(AzToolsFramework::GetActiveWindow(), "Pick *.csv file path.", "", "Pick *.csv file path."); if (fileName.isEmpty()) { - QMessageBox::warning(AzToolsFramework::GetActiveWindow(), "Error", "Please specify file", QMessageBox::Ok); + QMessageBox::warning(AzToolsFramework::GetActiveWindow(), "Error", "Please specify file path!", QMessageBox::Ok); return AZ::Edit::PropertyRefreshLevels::None; } diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h index fbd64fe8..683dc472 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h @@ -34,6 +34,6 @@ namespace FPSProfiler Configs::PrecisionSettings m_configPrecision; //!< Stores editor settings for the profiler Configs::DebugSettings m_configDebug; //!< Stores editor settings for the profiler - AZ::u32 PickOrCreateCsvFile(); + AZ::u32 SelectCsvPath(); }; } // namespace FPSProfiler From eb1bfc5ddb57c5df9acdfaee33c52105e410d57d Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 10:20:52 +0100 Subject: [PATCH 132/175] clang format Signed-off-by: Wojciech Czerski --- .../Clients/FPSProfilerSystemComponent.cpp | 20 ++++++++++--------- .../FPSProfilerEditorSystemComponent.cpp | 6 +++--- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp index d7ce54be..5422a330 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp @@ -435,9 +435,10 @@ namespace FPSProfiler char timestamp[20]; strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", &timeInfo); - static_cast(m_configFile.m_OutputFilename).ReplaceFilename( - (static_cast(m_configFile.m_OutputFilename).Stem().String() + "_" + timestamp + static_cast(m_configFile.m_OutputFilename).Extension().String()) - .data()); + static_cast(m_configFile.m_OutputFilename) + .ReplaceFilename((static_cast(m_configFile.m_OutputFilename).Stem().String() + "_" + timestamp + + static_cast(m_configFile.m_OutputFilename).Extension().String()) + .data()); } // Write profiling headers to file @@ -484,13 +485,14 @@ namespace FPSProfiler { AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); - if (path.empty() || !static_cast(path).HasFilename() || !static_cast(path).HasExtension() || !fileIO || !fileIO->ResolvePath(path.c_str())) + if (path.empty() || !static_cast(path).HasFilename() || !static_cast(path).HasExtension() || !fileIO || + !fileIO->ResolvePath(path.c_str())) { - const char* reason = path.empty() ? "Path cannot be empty." - : !static_cast(path).HasFilename() ? "Path must have a file at the end." - : !static_cast(path).HasExtension() ? "Path must have a *.csv extension." - : !fileIO ? "Could not get a FileIO object. Try again." - : "Path is not registered or recognizable by O3DE FileIO System."; + const char* reason = path.empty() ? "Path cannot be empty." + : !static_cast(path).HasFilename() ? "Path must have a file at the end." + : !static_cast(path).HasExtension() ? "Path must have a *.csv extension." + : !fileIO ? "Could not get a FileIO object. Try again." + : "Path is not registered or recognizable by O3DE FileIO System."; AZ_Warning("FPSProfiler::ChangeSavePath", !m_configDebug.m_PrintDebugInfo, "%s", reason); return false; diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp index 95b91306..3f500c85 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp @@ -33,7 +33,7 @@ namespace FPSProfiler ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->UIElement(AZ::Edit::UIHandlers::Button, "", "") - ->Attribute(AZ::Edit::Attributes::ButtonText, "Pick or Create a CSV") + ->Attribute(AZ::Edit::Attributes::ButtonText, "Select Csv File Path") ->Attribute(AZ::Edit::Attributes::ChangeNotify, &FPSProfilerEditorSystemComponent::SelectCsvPath) ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configFile) ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configRecord) @@ -70,7 +70,7 @@ namespace FPSProfiler AZ::u32 FPSProfilerEditorSystemComponent::SelectCsvPath() { - QString fileName = QFileDialog::getSaveFileName(AzToolsFramework::GetActiveWindow(), "Pick *.csv file path.", "", "Pick *.csv file path."); + QString fileName = QFileDialog::getSaveFileName(AzToolsFramework::GetActiveWindow(), "Pick a csv file path.", "", "*.csv"); if (fileName.isEmpty()) { @@ -81,7 +81,7 @@ namespace FPSProfiler // Ensure the file has the .csv extension if (!fileName.endsWith(".csv", Qt::CaseInsensitive)) { - fileName += ".csv"; // Auto-append .csv if missing + fileName += ".csv"; // Auto-append .csv if missing } m_configFile.m_OutputFilename = AZStd::string(fileName.toUtf8().constData()); From 9caff34a0db704bb6191ca665a2bc17951ad0233 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 10:38:55 +0100 Subject: [PATCH 133/175] remove system in name suffix Signed-off-by: Wojciech Czerski --- ...Component.cpp => FPSProfilerComponent.cpp} | 76 +++++++++---------- ...stemComponent.h => FPSProfilerComponent.h} | 10 +-- .../Code/Source/Clients/FPSProfilerModule.cpp | 2 +- .../Source/FPSProfilerModuleInterface.cpp | 8 +- ...ent.cpp => FPSProfilerEditorComponent.cpp} | 45 +++++------ ...mponent.h => FPSProfilerEditorComponent.h} | 10 +-- .../Source/Tools/FPSProfilerEditorModule.cpp | 6 +- .../fpsprofiler_editor_private_files.cmake | 4 +- .../Code/fpsprofiler_private_files.cmake | 4 +- 9 files changed, 82 insertions(+), 83 deletions(-) rename Gems/FPSProfiler/Code/Source/Clients/{FPSProfilerSystemComponent.cpp => FPSProfilerComponent.cpp} (84%) rename Gems/FPSProfiler/Code/Source/Clients/{FPSProfilerSystemComponent.h => FPSProfilerComponent.h} (93%) rename Gems/FPSProfiler/Code/Source/Tools/{FPSProfilerEditorSystemComponent.cpp => FPSProfilerEditorComponent.cpp} (60%) rename Gems/FPSProfiler/Code/Source/Tools/{FPSProfilerEditorSystemComponent.h => FPSProfilerEditorComponent.h} (72%) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp similarity index 84% rename from Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp rename to Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp index 5422a330..9f769794 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp @@ -1,4 +1,4 @@ -#include "FPSProfilerSystemComponent.h" +#include "FPSProfilerComponent.h" #include #include @@ -9,7 +9,7 @@ namespace FPSProfiler { - void FPSProfilerSystemComponent::Reflect(AZ::ReflectContext* context) + void FPSProfilerComponent::Reflect(AZ::ReflectContext* context) { Configs::FileSaveSettings::Reflect(context); Configs::RecordSettings::Reflect(context); @@ -18,26 +18,26 @@ namespace FPSProfiler if (auto serializeContext = azrtti_cast(context)) { - serializeContext->Class() + serializeContext->Class() ->Version(0) - ->Field("m_configFile", &FPSProfilerSystemComponent::m_configFile) - ->Field("m_configRecord", &FPSProfilerSystemComponent::m_configRecord) - ->Field("m_configPrecision", &FPSProfilerSystemComponent::m_configPrecision) - ->Field("m_configDebug", &FPSProfilerSystemComponent::m_configDebug); + ->Field("m_configFile", &FPSProfilerComponent::m_configFile) + ->Field("m_configRecord", &FPSProfilerComponent::m_configRecord) + ->Field("m_configPrecision", &FPSProfilerComponent::m_configPrecision) + ->Field("m_configDebug", &FPSProfilerComponent::m_configDebug); } } - void FPSProfilerSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + void FPSProfilerComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) { provided.push_back(AZ_CRC_CE("FPSProfilerService")); } - void FPSProfilerSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + void FPSProfilerComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) { incompatible.push_back(AZ_CRC_CE("FPSProfilerService")); } - FPSProfilerSystemComponent::FPSProfilerSystemComponent() + FPSProfilerComponent::FPSProfilerSystemComponent() { if (!FPSProfilerInterface::Get()) { @@ -45,7 +45,7 @@ namespace FPSProfiler } } - FPSProfilerSystemComponent::FPSProfilerSystemComponent( + FPSProfilerComponent::FPSProfilerSystemComponent( const Configs::FileSaveSettings& configF, const Configs::RecordSettings& configS, const Configs::PrecisionSettings& configP, @@ -61,7 +61,7 @@ namespace FPSProfiler } } - FPSProfilerSystemComponent::~FPSProfilerSystemComponent() + FPSProfilerComponent::~FPSProfilerSystemComponent() { if (FPSProfilerInterface::Get() == this) { @@ -69,7 +69,7 @@ namespace FPSProfiler } } - void FPSProfilerSystemComponent::Activate() + void FPSProfilerComponent::Activate() { if (!IsPathValid(m_configFile.m_OutputFilename)) { @@ -100,7 +100,7 @@ namespace FPSProfiler AZ_Printf("FPS Profiler", "FPSProfiler activated."); } - void FPSProfilerSystemComponent::Deactivate() + void FPSProfilerComponent::Deactivate() { AZ::TickBus::Handler::BusDisconnect(); @@ -114,7 +114,7 @@ namespace FPSProfiler FPSProfilerRequestBus::Handler::BusDisconnect(); } - void FPSProfilerSystemComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) + void FPSProfilerComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { if (!m_isProfiling) { @@ -197,12 +197,12 @@ namespace FPSProfiler } } - int FPSProfilerSystemComponent::GetTickOrder() + int FPSProfilerComponent::GetTickOrder() { return AZ::TICK_GAME; } - void FPSProfilerSystemComponent::StartProfiling() + void FPSProfilerComponent::StartProfiling() { if (m_isProfiling) { @@ -225,7 +225,7 @@ namespace FPSProfiler AZ_Printf("FPS Profiler", "Profiling started."); } - void FPSProfilerSystemComponent::StopProfiling() + void FPSProfilerComponent::StopProfiling() { if (!m_isProfiling) { @@ -247,7 +247,7 @@ namespace FPSProfiler AZ_Printf("FPS Profiler", "Profiling stopped."); } - void FPSProfilerSystemComponent::ResetProfilingData() + void FPSProfilerComponent::ResetProfilingData() { m_minFps = AZ::Constants::FloatMax; // Start at max to ensure the first valid FPS sets the minimum correctly in AZStd::min m_maxFps = 0.0f; @@ -264,17 +264,17 @@ namespace FPSProfiler AZ_Printf("FPS Profiler", "Profiling data reseted."); } - bool FPSProfilerSystemComponent::IsProfiling() const + bool FPSProfilerComponent::IsProfiling() const { return m_isProfiling; } - bool FPSProfilerSystemComponent::IsAnySaveOptionEnabled() const + bool FPSProfilerComponent::IsAnySaveOptionEnabled() const { return m_configRecord.m_RecordStats != Configs::RecordStatistics::None; } - void FPSProfilerSystemComponent::ChangeSavePath(const AZStd::string& newSavePath) + void FPSProfilerComponent::ChangeSavePath(const AZStd::string& newSavePath) { if (!IsPathValid(newSavePath)) { @@ -285,34 +285,34 @@ namespace FPSProfiler AZ_Warning("FPS Profiler", !m_configDebug.m_PrintDebugInfo && !m_isProfiling, "Path changed during activated profiling."); } - void FPSProfilerSystemComponent::SafeChangeSavePath(const AZStd::string& newSavePath) + void FPSProfilerComponent::SafeChangeSavePath(const AZStd::string& newSavePath) { // If profiling is enabled, save current opened file and stop profiling. StopProfiling(); ChangeSavePath(newSavePath); } - float FPSProfilerSystemComponent::GetMinFps() const + float FPSProfilerComponent::GetMinFps() const { return m_minFps; } - float FPSProfilerSystemComponent::GetMaxFps() const + float FPSProfilerComponent::GetMaxFps() const { return m_maxFps; } - float FPSProfilerSystemComponent::GetAvgFps() const + float FPSProfilerComponent::GetAvgFps() const { return m_avgFps; } - float FPSProfilerSystemComponent::GetCurrentFps() const + float FPSProfilerComponent::GetCurrentFps() const { return m_currentFps; } - AZStd::pair FPSProfilerSystemComponent::GetCpuMemoryUsed() const + AZStd::pair FPSProfilerComponent::GetCpuMemoryUsed() const { AZStd::size_t usedBytes = 0; AZStd::size_t reservedBytes = 0; @@ -321,7 +321,7 @@ namespace FPSProfiler return { usedBytes, reservedBytes }; } - AZStd::pair FPSProfilerSystemComponent::GetGpuMemoryUsed() const + AZStd::pair FPSProfilerComponent::GetGpuMemoryUsed() const { if (AZ::RHI::RHISystemInterface* rhiSystem = AZ::RHI::RHISystemInterface::Get()) { @@ -338,12 +338,12 @@ namespace FPSProfiler return { 0, 0 }; } - void FPSProfilerSystemComponent::SaveLogToFile() + void FPSProfilerComponent::SaveLogToFile() { WriteDataToFile(); } - void FPSProfilerSystemComponent::SaveLogToFileWithNewPath(const AZStd::string& newSavePath, bool useSafeChangePath) + void FPSProfilerComponent::SaveLogToFileWithNewPath(const AZStd::string& newSavePath, bool useSafeChangePath) { if (useSafeChangePath) { @@ -357,12 +357,12 @@ namespace FPSProfiler WriteDataToFile(); } - void FPSProfilerSystemComponent::ShowFpsOnScreen(bool enable) + void FPSProfilerComponent::ShowFpsOnScreen(bool enable) { m_configDebug.m_ShowFps = enable; } - void FPSProfilerSystemComponent::CalculateFpsData(const float& deltaTime) + void FPSProfilerComponent::CalculateFpsData(const float& deltaTime) { m_currentFps = deltaTime > 0 ? (1.0f / deltaTime) : 0.0f; m_totalFrameTime += deltaTime; @@ -404,7 +404,7 @@ namespace FPSProfiler m_maxFps = AZStd::max(m_maxFps, m_currentFps); } - void FPSProfilerSystemComponent::CreateLogFile() + void FPSProfilerComponent::CreateLogFile() { if (!IsAnySaveOptionEnabled()) { @@ -452,7 +452,7 @@ namespace FPSProfiler FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileCreated, m_configFile.m_OutputFilename.c_str()); } - void FPSProfilerSystemComponent::WriteDataToFile() + void FPSProfilerComponent::WriteDataToFile() { if (m_logBuffer.empty()) { @@ -476,12 +476,12 @@ namespace FPSProfiler FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileUpdate, m_configFile.m_OutputFilename.c_str()); } - float FPSProfilerSystemComponent::BytesToMB(AZStd::size_t bytes) + float FPSProfilerComponent::BytesToMB(AZStd::size_t bytes) { return static_cast(bytes) / (1024.0f * 1024.0f); } - bool FPSProfilerSystemComponent::IsPathValid(const AZStd::string& path) const + bool FPSProfilerComponent::IsPathValid(const AZStd::string& path) const { AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); @@ -501,7 +501,7 @@ namespace FPSProfiler return true; } - void FPSProfilerSystemComponent::ShowFps() const + void FPSProfilerComponent::ShowFps() const { AzFramework::DebugDisplayRequestBus::BusPtr debugDisplayBus; AzFramework::DebugDisplayRequestBus::Bind(debugDisplayBus, AzFramework::g_defaultSceneEntityDebugDisplayId); diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h similarity index 93% rename from Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h rename to Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h index ac5eb9f1..89018159 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h @@ -9,26 +9,26 @@ namespace FPSProfiler { - class FPSProfilerSystemComponent final + class FPSProfilerComponent final : public AZ::Component , protected FPSProfilerRequestBus::Handler , public AZ::TickBus::Handler { public: - AZ_COMPONENT(FPSProfilerSystemComponent, FPSProfilerSystemComponentTypeId, Component); + AZ_COMPONENT(FPSProfilerComponent, FPSProfilerSystemComponentTypeId, Component); static void Reflect(AZ::ReflectContext* context); static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); - FPSProfilerSystemComponent(); - explicit FPSProfilerSystemComponent( + FPSProfilerComponent(); + explicit FPSProfilerComponent( const Configs::FileSaveSettings& configF, const Configs::RecordSettings& configS, const Configs::PrecisionSettings& configP, const Configs::DebugSettings& configD); - ~FPSProfilerSystemComponent() override; + ~FPSProfilerComponent() override; protected: // AZ::Component interface implementation diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerModule.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerModule.cpp index e55db965..51b61564 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerModule.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerModule.cpp @@ -1,5 +1,5 @@ -#include "FPSProfilerSystemComponent.h" +#include "FPSProfilerComponent.h" #include #include diff --git a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp index d97f8e80..968f1808 100644 --- a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp +++ b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp @@ -4,7 +4,7 @@ #include -#include +#include namespace FPSProfiler { @@ -16,19 +16,19 @@ namespace FPSProfiler { // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here. // Add ALL components descriptors associated with this gem to m_descriptors. - // This will associate the AzTypeInfo information for the components with the the SerializeContext, BehaviorContext and EditContext. + // This will associate the AzTypeInfo information for the components with the SerializeContext, BehaviorContext and EditContext. // This happens through the [MyComponent]::Reflect() function. m_descriptors.insert( m_descriptors.end(), { - FPSProfilerSystemComponent::CreateDescriptor(), + FPSProfilerComponent::CreateDescriptor(), }); } AZ::ComponentTypeList FPSProfilerModuleInterface::GetRequiredSystemComponents() const { return AZ::ComponentTypeList{ - azrtti_typeid(), + azrtti_typeid(), }; } } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp similarity index 60% rename from Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp rename to Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp index 3f500c85..cf997676 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp @@ -1,5 +1,5 @@ -#include "FPSProfilerEditorSystemComponent.h" -#include +#include "FPSProfilerEditorComponent.h" +#include #include #include @@ -8,7 +8,7 @@ namespace FPSProfiler { - void FPSProfilerEditorSystemComponent::Reflect(AZ::ReflectContext* context) + void FPSProfilerEditorComponent::Reflect(AZ::ReflectContext* context) { Configs::FileSaveSettings::Reflect(context); Configs::RecordSettings::Reflect(context); @@ -17,58 +17,59 @@ namespace FPSProfiler if (auto serializeContext = azrtti_cast(context)) { - serializeContext->Class() + serializeContext->Class() ->Version(0) - ->Field("m_configFile", &FPSProfilerEditorSystemComponent::m_configFile) - ->Field("m_configRecord", &FPSProfilerEditorSystemComponent::m_configRecord) - ->Field("m_configPrecision", &FPSProfilerEditorSystemComponent::m_configPrecision) - ->Field("m_configDebug", &FPSProfilerEditorSystemComponent::m_configDebug); + ->Field("m_configFileEditor", &FPSProfilerEditorComponent::m_configFile) + ->Field("m_configRecordEditor", &FPSProfilerEditorComponent::m_configRecord) + ->Field("m_configPrecisionEditor", &FPSProfilerEditorComponent::m_configPrecision) + ->Field("m_configDebugEditor", &FPSProfilerEditorComponent::m_configDebug); if (AZ::EditContext* editContext = serializeContext->GetEditContext()) { - editContext - ->Class("FPS Profiler", "Tracks FPS, GPU and CPU performance and saves it into .csv") + editContext->Class("FPS Profiler", "Tracks FPS, GPU and CPU performance and saves it into .csv") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::Category, "Performance") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ->UIElement(AZ::Edit::UIHandlers::Button, "", "") ->Attribute(AZ::Edit::Attributes::ButtonText, "Select Csv File Path") - ->Attribute(AZ::Edit::Attributes::ChangeNotify, &FPSProfilerEditorSystemComponent::SelectCsvPath) - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configFile) - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configRecord) - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configPrecision) - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorSystemComponent::m_configDebug); + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &FPSProfilerEditorComponent::SelectCsvPath) + + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorComponent::m_configFile) + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorComponent::m_configRecord) + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorComponent::m_configPrecision) + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorComponent::m_configDebug); } } } - void FPSProfilerEditorSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + void FPSProfilerEditorComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) { provided.push_back(AZ_CRC_CE("FPSProfilerEditorService")); } - void FPSProfilerEditorSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + void FPSProfilerEditorComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) { incompatible.push_back(AZ_CRC_CE("FPSProfilerEditorService")); } - void FPSProfilerEditorSystemComponent::Activate() + void FPSProfilerEditorComponent::Activate() { AzToolsFramework::EditorEvents::Bus::Handler::BusConnect(); } - void FPSProfilerEditorSystemComponent::Deactivate() + void FPSProfilerEditorComponent::Deactivate() { AzToolsFramework::EditorEvents::Bus::Handler::BusDisconnect(); } - void FPSProfilerEditorSystemComponent::BuildGameEntity(AZ::Entity* entity) + void FPSProfilerEditorComponent::BuildGameEntity(AZ::Entity* entity) { - entity->CreateComponent(m_configFile, m_configRecord, m_configPrecision, m_configDebug); + entity->CreateComponent(m_configFile, m_configRecord, m_configPrecision, m_configDebug); } - AZ::u32 FPSProfilerEditorSystemComponent::SelectCsvPath() + AZ::u32 FPSProfilerEditorComponent::SelectCsvPath() { QString fileName = QFileDialog::getSaveFileName(AzToolsFramework::GetActiveWindow(), "Pick a csv file path.", "", "*.csv"); diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.h similarity index 72% rename from Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h rename to Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.h index 683dc472..98221f7b 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorSystemComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.h @@ -8,17 +8,15 @@ namespace FPSProfiler { /// System component for FPSProfiler editor - class FPSProfilerEditorSystemComponent - : public AzToolsFramework::Components::EditorComponentBase - , protected AzToolsFramework::EditorEvents::Bus::Handler + class FPSProfilerEditorComponent : public AzToolsFramework::Components::EditorComponentBase { public: - AZ_EDITOR_COMPONENT(FPSProfilerEditorSystemComponent, FPSProfilerEditorSystemComponentTypeId, EditorComponentBase); + AZ_EDITOR_COMPONENT(FPSProfilerEditorComponent, FPSProfilerEditorSystemComponentTypeId, EditorComponentBase); static void Reflect(AZ::ReflectContext* context); - FPSProfilerEditorSystemComponent() = default; - ~FPSProfilerEditorSystemComponent() override = default; + FPSProfilerEditorComponent() = default; + ~FPSProfilerEditorComponent() override = default; // AZ::Component void Activate() override; diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp index d83c271d..7e919588 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp @@ -1,5 +1,5 @@ -#include "FPSProfilerEditorSystemComponent.h" +#include "FPSProfilerEditorComponent.h" #include #include @@ -20,7 +20,7 @@ namespace FPSProfiler m_descriptors.insert( m_descriptors.end(), { - FPSProfilerEditorSystemComponent::CreateDescriptor(), + FPSProfilerEditorComponent::CreateDescriptor(), }); } @@ -31,7 +31,7 @@ namespace FPSProfiler AZ::ComponentTypeList GetRequiredSystemComponents() const override { return AZ::ComponentTypeList{ - azrtti_typeid(), + azrtti_typeid(), }; } }; diff --git a/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake index 934da04a..4c599923 100644 --- a/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake +++ b/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake @@ -1,5 +1,5 @@ set(FILES - Source/Tools/FPSProfilerEditorSystemComponent.cpp - Source/Tools/FPSProfilerEditorSystemComponent.h + Source/Tools/FPSProfilerEditorComponent.cpp + Source/Tools/FPSProfilerEditorComponent.h ) diff --git a/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake index b9e62b61..fe67fb56 100644 --- a/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake +++ b/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake @@ -2,8 +2,8 @@ set(FILES Source/FPSProfilerModuleInterface.cpp Source/FPSProfilerModuleInterface.h - Source/Clients/FPSProfilerSystemComponent.cpp - Source/Clients/FPSProfilerSystemComponent.h + Source/Clients/FPSProfilerComponent.cpp + Source/Clients/FPSProfilerComponent.h Source/Configurations/FPSProfilerConfig.cpp Source/Configurations/FPSProfilerConfig.h ) From e82b2f024976b34578d4ddcaa9c87293e22f5433 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 10:47:02 +0100 Subject: [PATCH 134/175] use path.c_str Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerComponent.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp index 9f769794..2837ba61 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp @@ -485,12 +485,12 @@ namespace FPSProfiler { AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); - if (path.empty() || !static_cast(path).HasFilename() || !static_cast(path).HasExtension() || !fileIO || + if (path.empty() || !static_cast(path.c_str()).HasFilename() || !static_cast(path.c_str()).HasExtension() || !fileIO || !fileIO->ResolvePath(path.c_str())) { const char* reason = path.empty() ? "Path cannot be empty." - : !static_cast(path).HasFilename() ? "Path must have a file at the end." - : !static_cast(path).HasExtension() ? "Path must have a *.csv extension." + : !static_cast(path.c_str()).HasFilename() ? "Path must have a file at the end." + : !static_cast(path.c_str()).HasExtension() ? "Path must have a *.csv extension." : !fileIO ? "Could not get a FileIO object. Try again." : "Path is not registered or recognizable by O3DE FileIO System."; From 00ebce031c8b151b52296c2530a9b3cdf027b1d9 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 10:48:37 +0100 Subject: [PATCH 135/175] rename System Component -> Component | remove unused Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerComponent.cpp | 6 +++--- .../Code/Source/Tools/FPSProfilerEditorComponent.cpp | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp index 2837ba61..f4e14183 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp @@ -37,7 +37,7 @@ namespace FPSProfiler incompatible.push_back(AZ_CRC_CE("FPSProfilerService")); } - FPSProfilerComponent::FPSProfilerSystemComponent() + FPSProfilerComponent::FPSProfilerComponent() { if (!FPSProfilerInterface::Get()) { @@ -45,7 +45,7 @@ namespace FPSProfiler } } - FPSProfilerComponent::FPSProfilerSystemComponent( + FPSProfilerComponent::FPSProfilerComponent( const Configs::FileSaveSettings& configF, const Configs::RecordSettings& configS, const Configs::PrecisionSettings& configP, @@ -61,7 +61,7 @@ namespace FPSProfiler } } - FPSProfilerComponent::~FPSProfilerSystemComponent() + FPSProfilerComponent::~FPSProfilerComponent() { if (FPSProfilerInterface::Get() == this) { diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp index cf997676..4da4b769 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp @@ -56,12 +56,10 @@ namespace FPSProfiler void FPSProfilerEditorComponent::Activate() { - AzToolsFramework::EditorEvents::Bus::Handler::BusConnect(); } void FPSProfilerEditorComponent::Deactivate() { - AzToolsFramework::EditorEvents::Bus::Handler::BusDisconnect(); } void FPSProfilerEditorComponent::BuildGameEntity(AZ::Entity* entity) From 75f7a23221ea6ea533833fd1efca8a50b1308ecb Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 11:04:43 +0100 Subject: [PATCH 136/175] rename type ids | fix uuid duplicate Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerTypeIds.h | 6 +++--- Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h | 2 +- .../Code/Source/Tools/FPSProfilerEditorComponent.h | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h index 0d3d13fb..69fcf734 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h @@ -4,11 +4,11 @@ namespace FPSProfiler { // System Component TypeIds - inline constexpr const char* FPSProfilerSystemComponentTypeId = "{B11CE88E-E1C5-404F-83D2-0D3850445A13}"; - inline constexpr const char* FPSProfilerEditorSystemComponentTypeId = "{F4308920-CD0B-4A2E-91DE-2EC1E970F97A}"; + inline constexpr const char* FPSProfilerComponentTypeId = "{B11CE88E-E1C5-404F-83D2-0D3850445A13}"; + inline constexpr const char* FPSProfilerEditorComponentTypeId = "{F4308920-CD0B-4A2E-91DE-2EC1E970F97A}"; // Configs TypeIds - inline constexpr const char* FPSProfilerConfigFileTypeId = "{70857242-4363-403C-ACF1-4A401B1024B5}"; + inline constexpr const char* FPSProfilerConfigFileTypeId = "{68627A89-9426-4640-B460-63E6AA42CFBC}"; inline constexpr const char* FPSProfilerConfigRecordTypeId = "{3CD7901E-F8C2-493A-B182-7EB2BAD9FBFB}"; inline constexpr const char* FPSProfilerConfigPrecisionTypeId = "{0ADE9CF6-1FE2-49E5-AB8D-B240EBDB9C03}"; inline constexpr const char* FPSProfilerConfigDebugTypeId = "{974F1627-C476-4310-B72A-7842BF868EC2}"; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h index 89018159..c42050ca 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h @@ -15,7 +15,7 @@ namespace FPSProfiler , public AZ::TickBus::Handler { public: - AZ_COMPONENT(FPSProfilerComponent, FPSProfilerSystemComponentTypeId, Component); + AZ_COMPONENT(FPSProfilerComponent, FPSProfilerComponentTypeId, Component); static void Reflect(AZ::ReflectContext* context); diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.h index 98221f7b..23dd0773 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.h +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.h @@ -11,7 +11,7 @@ namespace FPSProfiler class FPSProfilerEditorComponent : public AzToolsFramework::Components::EditorComponentBase { public: - AZ_EDITOR_COMPONENT(FPSProfilerEditorComponent, FPSProfilerEditorSystemComponentTypeId, EditorComponentBase); + AZ_EDITOR_COMPONENT(FPSProfilerEditorComponent, FPSProfilerEditorComponentTypeId, EditorComponentBase); static void Reflect(AZ::ReflectContext* context); From 988f8e2d9958ae42890f58463334197bfc25ca9e Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 11:28:48 +0100 Subject: [PATCH 137/175] resolve serialize duplicates Signed-off-by: Wojciech Czerski --- .../Configurations/FPSProfilerConfig.cpp | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp index 12f3e6a9..171c28e2 100644 --- a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp @@ -8,6 +8,11 @@ namespace FPSProfiler::Configs { if (auto serializeContext = azrtti_cast(context)) { + if (serializeContext->FindClassData(azrtti_typeid())) // Prevent duplicate registration + { + return; + } + serializeContext->Class() ->Version(0) ->Field("m_OutputFilename", &FileSaveSettings::m_OutputFilename) @@ -62,6 +67,11 @@ namespace FPSProfiler::Configs { if (auto serializeContext = azrtti_cast(context)) { + if (serializeContext->FindClassData(azrtti_typeid())) // Prevent duplicate registration + { + return; + } + serializeContext->Class() ->Version(0) ->Field("m_recordType", &RecordSettings::m_recordType) @@ -126,6 +136,11 @@ namespace FPSProfiler::Configs { if (auto* serializeContext = azrtti_cast(context)) { + if (serializeContext->FindClassData(azrtti_typeid())) // Prevent duplicate registration + { + return; + } + serializeContext->Class() ->Version(0) ->Field("m_NearZeroPrecision", &PrecisionSettings::m_NearZeroPrecision) @@ -177,6 +192,11 @@ namespace FPSProfiler::Configs { if (auto* serializeContext = azrtti_cast(context)) { + if (serializeContext->FindClassData(azrtti_typeid())) // Prevent duplicate registration + { + return; + } + serializeContext->Class() ->Version(0) ->Field("PrintDebugInfo", &DebugSettings::m_PrintDebugInfo) From 7947cf675a2dbc2b03598fbeec49afb4f812cb8d Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 11:59:03 +0100 Subject: [PATCH 138/175] fix path selector and validation Signed-off-by: Wojciech Czerski --- .../Source/Clients/FPSProfilerComponent.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp index f4e14183..8cda5e5a 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp @@ -485,16 +485,17 @@ namespace FPSProfiler { AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); - if (path.empty() || !static_cast(path.c_str()).HasFilename() || !static_cast(path.c_str()).HasExtension() || !fileIO || - !fileIO->ResolvePath(path.c_str())) + AZ::IO::Path tempPath(path.c_str()); + + if (tempPath.empty() || !tempPath.HasFilename() || !tempPath.HasExtension() || !fileIO || !fileIO->ResolvePath(tempPath)) { - const char* reason = path.empty() ? "Path cannot be empty." - : !static_cast(path.c_str()).HasFilename() ? "Path must have a file at the end." - : !static_cast(path.c_str()).HasExtension() ? "Path must have a *.csv extension." - : !fileIO ? "Could not get a FileIO object. Try again." - : "Path is not registered or recognizable by O3DE FileIO System."; + const char* reason = tempPath.empty() ? "Path cannot be empty." + : !tempPath.HasFilename() ? "Path must have a file at the end." + : !tempPath.HasExtension() ? "Path must have a *.csv extension." + : !fileIO ? "Could not get a FileIO object. Try again." + : "Path is not registered or recognizable by O3DE FileIO System."; - AZ_Warning("FPSProfiler::ChangeSavePath", !m_configDebug.m_PrintDebugInfo, "%s", reason); + AZ_Warning("FPSProfiler::IsPathValid", !m_configDebug.m_PrintDebugInfo, "%s", reason); return false; } From 448e31cc11d0b0939dbad12eb18436d03e0ebf78 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 12:02:52 +0100 Subject: [PATCH 139/175] rename Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerComponent.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp index 8cda5e5a..6c37f6c0 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp @@ -484,16 +484,16 @@ namespace FPSProfiler bool FPSProfilerComponent::IsPathValid(const AZStd::string& path) const { AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); + AZ::IO::Path pathToValidate(path.c_str()); - AZ::IO::Path tempPath(path.c_str()); - - if (tempPath.empty() || !tempPath.HasFilename() || !tempPath.HasExtension() || !fileIO || !fileIO->ResolvePath(tempPath)) + if (pathToValidate.empty() || !pathToValidate.HasFilename() || !pathToValidate.HasExtension() || !fileIO || + !fileIO->ResolvePath(pathToValidate)) { - const char* reason = tempPath.empty() ? "Path cannot be empty." - : !tempPath.HasFilename() ? "Path must have a file at the end." - : !tempPath.HasExtension() ? "Path must have a *.csv extension." - : !fileIO ? "Could not get a FileIO object. Try again." - : "Path is not registered or recognizable by O3DE FileIO System."; + const char* reason = pathToValidate.empty() ? "Path cannot be empty." + : !pathToValidate.HasFilename() ? "Path must have a file at the end." + : !pathToValidate.HasExtension() ? "Path must have a *.csv extension." + : !fileIO ? "Could not get a FileIO object. Try again." + : "Path is not registered or recognizable by O3DE FileIO System."; AZ_Warning("FPSProfiler::IsPathValid", !m_configDebug.m_PrintDebugInfo, "%s", reason); return false; From 48e57d3595e2f3f1f9f2a3090aa7155d00523bb8 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 12:54:45 +0100 Subject: [PATCH 140/175] add lua and script canvas support Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 49 ++++++++++++++++++- .../Include/FPSProfiler/FPSProfilerTypeIds.h | 1 + .../Source/Clients/FPSProfilerComponent.cpp | 34 +++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index af9d12bb..964c6d8d 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -4,8 +4,8 @@ #include #include -#include #include +#include namespace FPSProfiler { @@ -197,4 +197,51 @@ namespace FPSProfiler using FPSProfilerNotificationBus = AZ::EBus; + class FPSProfilerNotificationBusHandler + : public FPSProfilerNotificationBus::Handler + , public AZ::BehaviorEBusHandler + { + public: + AZ_EBUS_BEHAVIOR_BINDER( + FPSProfilerNotificationBusHandler, + FPSProfilerNotificationBusHandlerTypeId, + AZ::SystemAllocator, + OnFileCreated, + OnFileUpdate, + OnFileSaved, + OnProfileStart, + OnProfileReset, + OnProfileStop); + + void OnFileCreated(const AZStd::string& filePath) override + { + Call(FN_OnFileCreated, filePath); + } + + void OnFileUpdate(const AZStd::string& filePath) override + { + Call(FN_OnFileUpdate, filePath); + } + + void OnFileSaved(const AZStd::string& filePath) override + { + Call(FN_OnFileSaved, filePath); + } + + void OnProfileStart(const Configs::FileSaveSettings& config) override + { + Call(FN_OnProfileStart, config); + } + + void OnProfileReset(const Configs::FileSaveSettings& config) override + { + Call(FN_OnProfileReset, config); + } + + void OnProfileStop(const Configs::FileSaveSettings& config) override + { + Call(FN_OnProfileStop, config); + } + }; + } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h index 69fcf734..bef5c3ea 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h @@ -24,4 +24,5 @@ namespace FPSProfiler // Interface TypeIds inline constexpr const char* FPSProfilerRequestsTypeId = "{D585EA71-B052-4C97-8647-4B3511CC7C5B}"; inline constexpr const char* FPSProfilerNotificationsTypeId = "{63E04945-AD56-4BB6-888E-41C2FA71CC2F}"; + inline constexpr const char* FPSProfilerNotificationBusHandlerTypeId = "{0D957B03-F245-40C5-B069-98344B68ED8F}"; } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp index 6c37f6c0..8b2f0f68 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp @@ -25,6 +25,40 @@ namespace FPSProfiler ->Field("m_configPrecision", &FPSProfilerComponent::m_configPrecision) ->Field("m_configDebug", &FPSProfilerComponent::m_configDebug); } + + // Reflect EBus for Lua and Script Canvas + if (auto behaviorContext = azrtti_cast(context)) + { + // Request Bus - Calls made to FPS Profiler + behaviorContext->EBus("FPSProfilerRequestBus") + ->Attribute(AZ::Script::Attributes::Category, "FPS Profiler") + ->Attribute(AZ::Script::Attributes::Module, "FPSProfiler") + ->Event("StartProfiling", &FPSProfilerRequests::StartProfiling) + ->Event("StopProfiling", &FPSProfilerRequests::StopProfiling) + ->Event("ResetProfilingData", &FPSProfilerRequests::ResetProfilingData) + ->Event("IsProfiling", &FPSProfilerRequests::IsProfiling) + ->Event("IsAnySaveOptionEnabled", &FPSProfilerRequests::IsAnySaveOptionEnabled) + ->Event("ChangeSavePath", &FPSProfilerRequests::ChangeSavePath) + ->Event("SafeChangeSavePath", &FPSProfilerRequests::SafeChangeSavePath) + ->Event("GetMinFps", &FPSProfilerRequests::GetMinFps) + ->Event("GetMaxFps", &FPSProfilerRequests::GetMaxFps) + ->Event("GetAvgFps", &FPSProfilerRequests::GetAvgFps) + ->Event("GetCurrentFps", &FPSProfilerRequests::GetCurrentFps) + ->Event("GetCpuMemoryUsed", &FPSProfilerRequests::GetCpuMemoryUsed) + ->Event("GetGpuMemoryUsed", &FPSProfilerRequests::GetGpuMemoryUsed) + ->Event("SaveLogToFile", &FPSProfilerRequests::SaveLogToFile) + ->Event("SaveLogToFileWithNewPath", &FPSProfilerRequests::SaveLogToFileWithNewPath) + ->Event("ShowFpsOnScreen", &FPSProfilerRequests::ShowFpsOnScreen); + + behaviorContext->EBus("FPSProfilerNotificationBus") + ->Handler() + ->Event("OnFileCreated", &FPSProfilerNotifications::OnFileCreated) + ->Event("OnFileUpdate", &FPSProfilerNotifications::OnFileUpdate) + ->Event("OnFileSaved", &FPSProfilerNotifications::OnFileSaved) + ->Event("OnProfileStart", &FPSProfilerNotifications::OnProfileStart) + ->Event("OnProfileReset", &FPSProfilerNotifications::OnProfileReset) + ->Event("OnProfileStop", &FPSProfilerNotifications::OnProfileStop); + } } void FPSProfilerComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) From b54c6635a3895e3e2bfb4fcd5b91267c83cda9cc Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 13:15:41 +0100 Subject: [PATCH 141/175] fix path swap to string | operate on IO::Path Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerComponent.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp index 8b2f0f68..ab298092 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp @@ -469,10 +469,10 @@ namespace FPSProfiler char timestamp[20]; strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", &timeInfo); - static_cast(m_configFile.m_OutputFilename) - .ReplaceFilename((static_cast(m_configFile.m_OutputFilename).Stem().String() + "_" + timestamp + - static_cast(m_configFile.m_OutputFilename).Extension().String()) - .data()); + AZ::IO::Path logFilePath(m_configFile.m_OutputFilename); + logFilePath.ReplaceFilename((logFilePath.Stem().String() + "_" + timestamp + logFilePath.Extension().String()).data()); + + m_configFile.m_OutputFilename = logFilePath.c_str(); } // Write profiling headers to file From 0cb786e89a85a2fefc981331ab3953928c4bc493 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 13:32:49 +0100 Subject: [PATCH 142/175] option to keep hisotry for better precision Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp | 2 +- .../Code/Source/Configurations/FPSProfilerConfig.cpp | 4 ++-- .../Code/Source/Configurations/FPSProfilerConfig.h | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp index ab298092..2b2121d9 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp @@ -404,7 +404,7 @@ namespace FPSProfiler // Latest fps history for avg fps calculation m_fpsSamples.push_back(m_currentFps); - if (m_fpsSamples.size() > m_configFile.m_AutoSaveAtFrame) + if (!m_configPrecision.m_keepHistory && m_fpsSamples.size() > m_configFile.m_AutoSaveAtFrame) { m_fpsSamples.pop_front(); } diff --git a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp index 171c28e2..bb29cde1 100644 --- a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp @@ -146,7 +146,7 @@ namespace FPSProfiler::Configs ->Field("m_NearZeroPrecision", &PrecisionSettings::m_NearZeroPrecision) ->Field("m_avgFpsType", &PrecisionSettings::m_avgFpsType) ->Field("m_smoothingFactor", &PrecisionSettings::m_smoothingFactor) - ->Field("m_useAvgMedianFilter", &PrecisionSettings::m_useAvgMedianFilter); + ->Field("m_keepHistory", &PrecisionSettings::m_keepHistory); if (auto* editContext = serializeContext->GetEditContext()) { @@ -181,7 +181,7 @@ namespace FPSProfiler::Configs }) ->DataElement( AZ::Edit::UIHandlers::CheckBox, - &PrecisionSettings::m_useAvgMedianFilter, + &PrecisionSettings::m_keepHistory, "Use Average Median Filter", "Enable median filtering for averaging"); } diff --git a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.h index e1078d24..4cbe7c33 100644 --- a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.h +++ b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.h @@ -65,7 +65,7 @@ namespace FPSProfiler::Configs float m_NearZeroPrecision = 0.01f; MovingAverageType m_avgFpsType = MovingAverageType::Simple; float m_smoothingFactor = 2.0f; - bool m_useAvgMedianFilter = true; + bool m_keepHistory = false; }; struct DebugSettings From c6dcfe83411afb7eab5af0fe6731e9e89ce6ba13 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 13:37:50 +0100 Subject: [PATCH 143/175] fix keep history setting | fix reflect description Signed-off-by: Wojciech Czerski --- .../FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp | 3 ++- .../Code/Source/Configurations/FPSProfilerConfig.cpp | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp index 2b2121d9..ab4e66e9 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp @@ -404,7 +404,8 @@ namespace FPSProfiler // Latest fps history for avg fps calculation m_fpsSamples.push_back(m_currentFps); - if (!m_configPrecision.m_keepHistory && m_fpsSamples.size() > m_configFile.m_AutoSaveAtFrame) + if (m_configFile.m_AutoSaveAtFrame != 0.0f && !m_configPrecision.m_keepHistory && + m_fpsSamples.size() > m_configFile.m_AutoSaveAtFrame) { m_fpsSamples.pop_front(); } diff --git a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp index bb29cde1..d7a5a5be 100644 --- a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp @@ -182,8 +182,9 @@ namespace FPSProfiler::Configs ->DataElement( AZ::Edit::UIHandlers::CheckBox, &PrecisionSettings::m_keepHistory, - "Use Average Median Filter", - "Enable median filtering for averaging"); + "Keep History", + "Enabled saves entire history for better avg fps smoothing, otherwise history is cleared per auto save if " + "enabled."); } } } From 329ad2ff059835011fca9420e9652916a3cb4203 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 13:40:25 +0100 Subject: [PATCH 144/175] simplify logic | clang format Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp index ab4e66e9..fb84e80c 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp @@ -404,8 +404,7 @@ namespace FPSProfiler // Latest fps history for avg fps calculation m_fpsSamples.push_back(m_currentFps); - if (m_configFile.m_AutoSaveAtFrame != 0.0f && !m_configPrecision.m_keepHistory && - m_fpsSamples.size() > m_configFile.m_AutoSaveAtFrame) + if (!m_configFile.m_AutoSave && !m_configPrecision.m_keepHistory && m_fpsSamples.size() > m_configFile.m_AutoSaveAtFrame) { m_fpsSamples.pop_front(); } From 21cb54fc3667dde2371185e45bacb6b07fe027c2 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 14:06:49 +0100 Subject: [PATCH 145/175] fix game launcher | pass settings from editor Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerComponent.h | 8 +++----- .../Code/Source/Tools/FPSProfilerEditorComponent.cpp | 8 +++++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h index c42050ca..85e2c851 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h @@ -57,17 +57,16 @@ namespace FPSProfiler void SaveLogToFileWithNewPath(const AZStd::string& newSavePath, bool useSafeChangePath) override; void ShowFpsOnScreen(bool enable) override; - private: + public: // Profiler Configurations Configs::FileSaveSettings m_configFile; //!< Stores editor settings for the profiler Configs::RecordSettings m_configRecord; //!< Stores editor settings for the profiler Configs::PrecisionSettings m_configPrecision; //!< Stores editor settings for the profiler Configs::DebugSettings m_configDebug; //!< Stores editor settings for the profiler - // Profiling State - bool m_isProfiling = false; //!< Flag to indicate if profiling is active - + private: // FPS Tracking Data + bool m_isProfiling = false; //!< Flag to indicate if profiling is active float m_minFps = 0.0f; //!< Lowest FPS value recorded float m_maxFps = 0.0f; //!< Highest FPS value recorded float m_avgFps = 0.0f; //!< Mean value of collected FPS samples @@ -75,7 +74,6 @@ namespace FPSProfiler float m_totalFrameTime = 0.0f; //!< Time taken for the current frame int m_frameCount = 0; //!< Total number of frames processed int m_recordedFrameCount = 0; //!< Total number of frames recorded. Used when @ref m_configRecord.m_framesToRecord != 0 - AZStd::deque m_fpsSamples; //!< Stores recent FPS values for averaging // Log Buffer diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp index 4da4b769..4790ef96 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp @@ -64,7 +64,13 @@ namespace FPSProfiler void FPSProfilerEditorComponent::BuildGameEntity(AZ::Entity* entity) { - entity->CreateComponent(m_configFile, m_configRecord, m_configPrecision, m_configDebug); + if (FPSProfilerComponent* gameComponent = entity->CreateComponent()) + { + gameComponent->m_configFile = m_configFile; + gameComponent->m_configRecord = m_configRecord; + gameComponent->m_configPrecision = m_configPrecision; + gameComponent->m_configDebug = m_configDebug; + } } AZ::u32 FPSProfilerEditorComponent::SelectCsvPath() From 882dd9ff90f50c6310e91608f5447ae537970548 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 14:10:23 +0100 Subject: [PATCH 146/175] fix formatting | make fields protected Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h index 85e2c851..1ca8c8b0 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h @@ -12,7 +12,7 @@ namespace FPSProfiler class FPSProfilerComponent final : public AZ::Component , protected FPSProfilerRequestBus::Handler - , public AZ::TickBus::Handler + , protected AZ::TickBus::Handler { public: AZ_COMPONENT(FPSProfilerComponent, FPSProfilerComponentTypeId, Component); @@ -30,11 +30,11 @@ namespace FPSProfiler const Configs::DebugSettings& configD); ~FPSProfilerComponent() override; - protected: // AZ::Component interface implementation void Activate() override; void Deactivate() override; + protected: // AZTickBus interface implementation void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; int GetTickOrder() override; From 041b8db792c2ffc71f2a0a5cf99eb6d89e4af3af Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 14:16:30 +0100 Subject: [PATCH 147/175] add button description Signed-off-by: Wojciech Czerski --- .../Code/Source/Tools/FPSProfilerEditorComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp index 4790ef96..213c2352 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp @@ -32,7 +32,7 @@ namespace FPSProfiler ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) ->Attribute(AZ::Edit::Attributes::AutoExpand, true) - ->UIElement(AZ::Edit::UIHandlers::Button, "", "") + ->UIElement(AZ::Edit::UIHandlers::Button, "", "Click to open file dialog.") ->Attribute(AZ::Edit::Attributes::ButtonText, "Select Csv File Path") ->Attribute(AZ::Edit::Attributes::ChangeNotify, &FPSProfilerEditorComponent::SelectCsvPath) From e4c7513a9f9efb4002eb8aa00c703fba7fe12fc8 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 14:22:40 +0100 Subject: [PATCH 148/175] add better description Signed-off-by: Wojciech Czerski --- .../Code/Source/Configurations/FPSProfilerConfig.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp index d7a5a5be..5cde69af 100644 --- a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp @@ -37,7 +37,8 @@ namespace FPSProfiler::Configs AZ::Edit::UIHandlers::Default, &FileSaveSettings::m_AutoSave, "Auto Save", - "When enabled, system will auto save after specified frame occurrence.") + "When enabled, system will auto save after specified frame occurrence. Recommended for optimization and long " + "recordings.") ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree) ->DataElement( @@ -58,7 +59,8 @@ namespace FPSProfiler::Configs AZ::Edit::UIHandlers::Default, &FileSaveSettings::m_SaveWithTimestamp, "Timestamp", - "When enabled, system will save files with timestamp postfix of current date, hour, minutes and seconds."); + "When enabled, the system will save files with a timestamp postfix of the current date, hour, minutes, and " + "seconds. This allows you to save automatically without manual input each time."); } } } From fb9e9268bc340b9d39c62d101b765dd9838c6352 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 14:30:47 +0100 Subject: [PATCH 149/175] refactor Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h index 1ca8c8b0..c63f64ed 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h @@ -11,8 +11,8 @@ namespace FPSProfiler { class FPSProfilerComponent final : public AZ::Component - , protected FPSProfilerRequestBus::Handler , protected AZ::TickBus::Handler + , protected FPSProfilerRequestBus::Handler { public: AZ_COMPONENT(FPSProfilerComponent, FPSProfilerComponentTypeId, Component); From c038df116ccab311aa0bdbae1e4dc754053802de Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 14:53:10 +0100 Subject: [PATCH 150/175] fix config reflect Signed-off-by: Wojciech Czerski --- .../Source/Clients/FPSProfilerComponent.cpp | 2 +- .../Configurations/FPSProfilerConfig.cpp | 21 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp index fb84e80c..b0bcc9e8 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp @@ -26,7 +26,7 @@ namespace FPSProfiler ->Field("m_configDebug", &FPSProfilerComponent::m_configDebug); } - // Reflect EBus for Lua and Script Canvas + // EBus Reflect for Lua and Script Canvas if (auto behaviorContext = azrtti_cast(context)) { // Request Bus - Calls made to FPS Profiler diff --git a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp index 5cde69af..77c5f267 100644 --- a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp @@ -22,8 +22,8 @@ namespace FPSProfiler::Configs if (AZ::EditContext* editContext = serializeContext->GetEditContext()) { - editContext->Class("File Settings", "Settings controlling save file operations.") - ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + editContext->Class("File Save Settings", "Settings for managing how FPS data is saved to a file.") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "Configure file-saving options for recorded FPS data.") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->DataElement( @@ -83,8 +83,8 @@ namespace FPSProfiler::Configs if (auto* editContext = serializeContext->GetEditContext()) { - editContext->Class("Record Settings", "Settings controlling the recording behavior.") - ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + editContext->Class("Recording Settings", "Options for configuring how FPS data is recorded.") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "Control the behavior of FPS data recording.") ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->DataElement( @@ -152,12 +152,15 @@ namespace FPSProfiler::Configs if (auto* editContext = serializeContext->GetEditContext()) { - editContext->Class("Precision Settings", "Settings for FPS profiler precision") + editContext->Class("Precision Settings", "Defines the precision level of the FPS Profiler measurements.") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "Adjust how precisely the FPS Profiler records data.") + ->DataElement( AZ::Edit::UIHandlers::Default, &PrecisionSettings::m_NearZeroPrecision, "Near Zero Precision", "Threshold for near-zero values") + ->DataElement( AZ::Edit::UIHandlers::ComboBox, &PrecisionSettings::m_avgFpsType, @@ -166,6 +169,7 @@ namespace FPSProfiler::Configs ->EnumAttribute(MovingAverageType::Simple, "Simple") ->EnumAttribute(MovingAverageType::Exponential, "Exponential") ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree) + ->DataElement( AZ::Edit::UIHandlers::Default, &PrecisionSettings::m_smoothingFactor, @@ -181,6 +185,7 @@ namespace FPSProfiler::Configs return data && data->m_avgFpsType == Exponential ? AZ::Edit::PropertyVisibility::Show : AZ::Edit::PropertyVisibility::Hide; }) + ->DataElement( AZ::Edit::UIHandlers::CheckBox, &PrecisionSettings::m_keepHistory, @@ -208,13 +213,17 @@ namespace FPSProfiler::Configs if (auto* editContext = serializeContext->GetEditContext()) { - editContext->Class("Debug Settings", "Settings for debugging the FPS Profiler") + editContext->Class("Debug Settings", "Configuration options for debugging the FPS Profiler.") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "Customize debugging settings for the FPS Profiler.") + ->DataElement( AZ::Edit::UIHandlers::CheckBox, &DebugSettings::m_PrintDebugInfo, "Print Debug Info", "Enable or disable debug information printing") + ->DataElement(AZ::Edit::UIHandlers::CheckBox, &DebugSettings::m_ShowFps, "Show FPS", "Toggle FPS display on screen") + ->DataElement( AZ::Edit::UIHandlers::Color, &DebugSettings::m_Color, "Debug Color", "Set the debug information display color"); } From c32faf807001ea9c5ed930cbb61e0d9d770ea832 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Mon, 17 Mar 2025 15:26:37 +0100 Subject: [PATCH 151/175] add debug visiblity on enabled button Signed-off-by: Wojciech Czerski --- .../Source/Configurations/FPSProfilerConfig.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp index 77c5f267..d1fa22bc 100644 --- a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp @@ -187,7 +187,7 @@ namespace FPSProfiler::Configs }) ->DataElement( - AZ::Edit::UIHandlers::CheckBox, + AZ::Edit::UIHandlers::Default, &PrecisionSettings::m_keepHistory, "Keep History", "Enabled saves entire history for better avg fps smoothing, otherwise history is cleared per auto save if " @@ -220,12 +220,20 @@ namespace FPSProfiler::Configs AZ::Edit::UIHandlers::CheckBox, &DebugSettings::m_PrintDebugInfo, "Print Debug Info", - "Enable or disable debug information printing") + "Enable or disable debug information printing.") - ->DataElement(AZ::Edit::UIHandlers::CheckBox, &DebugSettings::m_ShowFps, "Show FPS", "Toggle FPS display on screen") + ->DataElement(AZ::Edit::UIHandlers::CheckBox, &DebugSettings::m_ShowFps, "Show FPS", "Toggle FPS display on screen.") + ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree) ->DataElement( - AZ::Edit::UIHandlers::Color, &DebugSettings::m_Color, "Debug Color", "Set the debug information display color"); + AZ::Edit::UIHandlers::Color, &DebugSettings::m_Color, "Debug Color", "Set the debug information display color.") + ->Attribute( + AZ::Edit::Attributes::Visibility, + [](const void* instance) + { + const DebugSettings* data = static_cast(instance); + return data && data->m_ShowFps ? AZ::Edit::PropertyVisibility::Show : AZ::Edit::PropertyVisibility::Hide; + }); } } } From 0017cf810302b7b0989dc2180b5173ad18fe64b6 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 18 Mar 2025 09:45:01 +0100 Subject: [PATCH 152/175] readme update Signed-off-by: Wojciech Czerski --- doc/FpsProfiler.png | Bin 34298 -> 50955 bytes doc/FpsProfiler_ScriptCanvas.png | Bin 0 -> 72560 bytes readme.md | 156 ++++++++++++++++++++++++------- 3 files changed, 121 insertions(+), 35 deletions(-) create mode 100644 doc/FpsProfiler_ScriptCanvas.png diff --git a/doc/FpsProfiler.png b/doc/FpsProfiler.png index 215f1956a965472e8562961b34b2bc1d5931d089..6f2a380c6e6db680d50cd0089b72a5a3b30e91db 100644 GIT binary patch literal 50955 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ΝI(Ozc+ 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 C0CTJW literal 34298 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 SOz>c!A8}zBp>hE||NjGIZvj#O diff --git a/doc/FpsProfiler_ScriptCanvas.png b/doc/FpsProfiler_ScriptCanvas.png new file mode 100644 index 0000000000000000000000000000000000000000..2997ce11c3790ff6a4f4092ed6d5d12151d94178 GIT binary patch literal 72560 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 *To start using the tool, add a `FPSProfiler` to the **Level** entity.* ![FpsProfiler Editor](doc/FpsProfiler.png) -| Variable Name | Description | -|----------------------------|----------------------------------------------------------------------------------------------------| -| **Csv Save Path** | Path where collected data will be saved. | -| **Auto Save** | Enable to auto save. Auto save is performed when target defined variable is reached. | -| **Auto Save At Frame** | Auto saves collected data at selected frame occurrence. | -| **Timestamp** | Applies timestamp Year-Month-Day-Hour-Minutes to file name. Let's you save multiple files at once. | -| **Near Zero Precision** | Floating point precision when comparing to 0. | -| **Save FPS Data** | Save collected FPS statistics. | -| **Save CPU Data** | Save collected CPU statistics. | -| **Save GPU Data** | Save collected GPU statistics. | -| **Show FPS** | Show the FPS value in the left-top corner. | -| **Profile On Game Start** | Start profiling data at once into csv file, after entering game mode. | - -## Setup -To start using the tool, add a `FPSProfiler` to the **Level** entity. - -### Profiling data using API Interface: +| File Save Settings | Description | +|----------------------|----------------------------------------------------------------------------------------------------------------| +| Select Csv File Path | Button that opens a File Dialog. | +| Csv Save Path | A path where *.csv will be saved. | +| Auto Save | Enables automatic saving of FPS logs per frame. | +| Auto Save At Frame | Specifies the frame interval for auto-saving. | +| Timestamp | Includes timestamps in the FPS log file name.
Allows to save automatically without manual input each time. | + +| Recording Settings | Description | +|------------------------|----------------------------------------------------------------------------------------------------------------------| +| Record Type | Specify when to start recording:
- at game start
- selected frame
- await for other system to call start | +| Frames To Skip | Number of frames to skip before recording. Only enabled when type is `SelectFrame`. | +| Frames To Record | Total number of frames to record. If set to 0 - unlimited. | +| Record Stats | Specifies what type of stats to record (FPS data, CPU and GPU). | + +| Precision Settings | Description | +|------------------------|--------------------------------------------------------------------------------------------------------| +| Near Zero Precision | Precision threshold for near-zero values. | +| Moving Average Type | Type of moving average used for smoothing average FPS:
- Simple
- Exponential | +| Alpha Smoothing Factor | Factor applied to control smoothing effect when `Exponential` enabled. | +| Keep History | Keeps a history of recorded FPS data or clear after every auto-save. For better effect - keep enabled. | + +| Debug Settings | Description | +|------------------------|-----------------------------------------| +| Print Debug Info | Displays debug information in the logs. | +| Show FPS | Enables FPS display on screen. | +| Debug Color | Color used for debugging FPS display. | + +## API Access +Example workflows how to access and use a Fps Profiler Events and Notifications. + +### In C++: ```c++ -// Get Interface and validate it +// Example with Interface auto profiler = FPSProfiler::FPSProfilerInterface::Get(); if (!profiler) { @@ -382,24 +400,92 @@ if (!profiler) profiler->StartProfiling(); float currentFps = profiler->GetCurrentFps(); -``` -### Profiling data using API Broadcast: -```c++ -// Start profiling -FPSProfilerRequestBus::Broadcast(&FPSProfilerRequests::StartProfiling); +// Example with Broadcast +float avgFPS = 0.0f; +FPSProfilerRequestBus::BroadcastResult(avgFPS, &FPSProfilerRequests::GetAvgFps); +FPSProfilerRequestBus::Broadcast(&FPSProfilerRequests::StopProfiling); + +// Notification Bus - override from FPSProfilerNotificationBus::Handler +// class YourClass : protected FPSProfilerNotificationBus::Handler -// Retrieve FPS using the request bus -float currentFps = 0.0f; -FPSProfilerRequestBus::BroadcastResult(currentFps, &FPSProfilerRequests::GetCurrentFps); +void OnProfileStart(const Configs::FileSaveSettings& config) override +{ + // Your logic ... +} ``` -## CSV File Example -| Frame | FrameTime | CurrentFPS | MinFPS | MaxFPS | AvgFPS | CpuMemoryUsed | CpuMemoryReserved | GpuMemoryUsed | GpuMemoryReserved | -|-------|-----------|------------|--------|--------|--------|---------------|-------------------|---------------|--------------------| -| 1 | 0.3943 | 2.54 | 2.54 | 2.54 | 2.54 | 2166.44 | 237568 | 3756.19 | 6930.19 | -| 2 | 0.1643 | 6.09 | 2.54 | 6.09 | 4.31 | 2182.99 | 237568 | 4126.19 | 6928.12 | -| 3 | 0.1150 | 8.69 | 2.54 | 8.69 | 5.77 | 2183.49 | 237568 | 3134.19 | 6928.69 | -| 4 | 0.0203 | 49.33 | 2.54 | 49.33 | 16.66 | 2181.58 | 237568 | 2654.19 | 6928.69 | -| 5 | 0.0282 | 35.46 | 2.54 | 49.33 | 20.42 | 2181.20 | 237568 | 2654.19 | 6928.69 | +### In Lua +```shell +-- Table to hold our script functions +local profilerScript = {} + +-- Function called when profiling starts +function profilerScript:OnProfileStart(config) + Debug.Log("Profiling started. Stopping in 60 seconds...") + + -- Start a 60-second timer before stopping profiling + self:StartTimer(60, function() + Debug.Log("Stopping profiling now...") + FPSProfilerRequestBus.Broadcast.StopProfiling() + end) +end + +-- Function to start a timer +function profilerScript:StartTimer(delay, callback) + if self.timerEventId then + -- Prevent multiple timers from stacking + TickBus.Disconnect(self, self.timerEventId) + end + + self.timerTimeRemaining = delay + self.timerCallback = callback + self.timerEventId = TickBus.Connect(self) +end + +-- Tick event to track time +function profilerScript:OnTick(deltaTime, timePoint) + if self.timerTimeRemaining then + self.timerTimeRemaining = self.timerTimeRemaining - deltaTime + if self.timerTimeRemaining <= 0 then + -- Time is up, trigger callback and disconnect + if self.timerCallback then + self.timerCallback() + end + TickBus.Disconnect(self, self.timerEventId) + self.timerEventId = nil + end + end +end + +-- Register as an FPSProfilerNotificationBus listener +function profilerScript:OnActivate() + FPSProfilerNotificationBus.Connect(self) +end + +-- Cleanup when script is deactivated +function profilerScript:OnDeactivate() + FPSProfilerNotificationBus.Disconnect(self) + + -- Disconnect timer if still active + if self.timerEventId then + TickBus.Disconnect(self, self.timerEventId) + end +end + +-- Return the table so O3DE can use it +return profilerScript +``` +### In Script Canvas +Example how to stop profiling after 60 seconds have passed in Script Canvas. +![FpsProfiler Editor](doc/FpsProfiler_ScriptCanvas.png) + +## Csv Output File - Example +| Frame | FrameTime | CurrentFPS | MinFPS | MaxFPS | AvgFPS | CpuMemoryUsed | CpuMemoryReserved | GpuMemoryUsed | GpuMemoryReserved | +|-------|-----------|-------------|--------|---------|---------|----------------|--------------------|----------------|--------------------| +| 1 | 0.0293 | 34.12 | 34.12 | 34.12 | 34.12 | 231.91 | 237568 | 691.44 | 7214 | +| 2 | 0.0054 | 185.53 | 34.12 | 185.53 | 37.11 | 232 | 237568 | 691.44 | 7214 | +| 3 | 0.005 | 199.2 | 34.12 | 199.2 | 40.32 | 231.95 | 237568 | 691.44 | 7214 | +| 4 | 0.0038 | 259.88 | 34.12 | 259.88 | 44.67 | 231.64 | 237568 | 691.44 | 7214 | +| 5 | 0.004 | 247.65 | 34.12 | 259.88 | 48.69 | 231.64 | 237568 | 691.44 | 7214 | From 77ca9e15a1f02c7781716bcb29790fd878cbea33 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 18 Mar 2025 09:50:08 +0100 Subject: [PATCH 153/175] readme table update Signed-off-by: Wojciech Czerski --- readme.md | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index 05e15cbb..3d254bf4 100644 --- a/readme.md +++ b/readme.md @@ -356,8 +356,10 @@ This functionality can be accessed in c++, lua and script canvas. > *To start using the tool, add a `FPSProfiler` to the **Level** entity.* +## Component Functionality ![FpsProfiler Editor](doc/FpsProfiler.png) +### Save Settings | File Save Settings | Description | |----------------------|----------------------------------------------------------------------------------------------------------------| | Select Csv File Path | Button that opens a File Dialog. | @@ -366,20 +368,23 @@ This functionality can be accessed in c++, lua and script canvas. | Auto Save At Frame | Specifies the frame interval for auto-saving. | | Timestamp | Includes timestamps in the FPS log file name.
Allows to save automatically without manual input each time. | +### Recording Settings | Recording Settings | Description | |------------------------|----------------------------------------------------------------------------------------------------------------------| | Record Type | Specify when to start recording:
- at game start
- selected frame
- await for other system to call start | -| Frames To Skip | Number of frames to skip before recording. Only enabled when type is `SelectFrame`. | +| Frames To Skip | Number of frames to skip before recording.
Only enabled when type is `SelectFrame`. | | Frames To Record | Total number of frames to record. If set to 0 - unlimited. | | Record Stats | Specifies what type of stats to record (FPS data, CPU and GPU). | -| Precision Settings | Description | -|------------------------|--------------------------------------------------------------------------------------------------------| -| Near Zero Precision | Precision threshold for near-zero values. | -| Moving Average Type | Type of moving average used for smoothing average FPS:
- Simple
- Exponential | -| Alpha Smoothing Factor | Factor applied to control smoothing effect when `Exponential` enabled. | -| Keep History | Keeps a history of recorded FPS data or clear after every auto-save. For better effect - keep enabled. | +### Precision Settings +| Precision Settings | Description | +|------------------------|------------------------------------------------------------------------------------------------------------| +| Near Zero Precision | Precision threshold for near-zero values. | +| Moving Average Type | Type of moving average used for smoothing average FPS:
- Simple
- Exponential | +| Alpha Smoothing Factor | Factor applied to control smoothing effect when `Exponential` enabled. | +| Keep History | Keeps a history of recorded FPS data or clear after every auto-save.
For better effect - keep enabled. | +### Debug Settings | Debug Settings | Description | |------------------------|-----------------------------------------| | Print Debug Info | Displays debug information in the logs. | From cddb207254288d1cc42a3d9e30b54aa9bb44c0b7 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 18 Mar 2025 09:56:07 +0100 Subject: [PATCH 154/175] update images resolution Signed-off-by: Wojciech Czerski --- doc/FpsProfiler.png | Bin 50955 -> 138418 bytes doc/FpsProfiler_ScriptCanvas.png | Bin 72560 -> 155917 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/FpsProfiler.png b/doc/FpsProfiler.png index 6f2a380c6e6db680d50cd0089b72a5a3b30e91db..7a41c787a4f0c92b932746720e88982a2330705f 100644 GIT binary patch literal 138418 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 literal 50955 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ΝI(Ozc+ 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 C0CTJW diff --git a/doc/FpsProfiler_ScriptCanvas.png b/doc/FpsProfiler_ScriptCanvas.png index 2997ce11c3790ff6a4f4092ed6d5d12151d94178..bd854ba0375a35a29f823bec89cb65b892fb6570 100644 GIT binary patch literal 155917 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�n@2znW(#JE!$+`(i}a@70lWq^%J2Mw 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 Date: Tue, 18 Mar 2025 11:13:22 +0100 Subject: [PATCH 155/175] add behaviour context for configs | update profile bus Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 68 ++++++++++++------- .../Source/Clients/FPSProfilerComponent.cpp | 13 ++-- .../Configurations/FPSProfilerConfig.cpp | 64 +++++++++++++++++ 3 files changed, 115 insertions(+), 30 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 964c6d8d..78ba601e 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -138,49 +138,62 @@ namespace FPSProfiler /** * @brief Called when a new file is created. - * @param filePath The path of the newly created file. + * @param config File Save Settings Configuration. */ - virtual void OnFileCreated(const AZStd::string& filePath) + virtual void OnFileCreated(const Configs::FileSaveSettings& config) { } /** * @brief Called when an existing file is updated. - * @param filePath The path of the file that was modified. + * @param config File Save Settings Configuration. */ - virtual void OnFileUpdate(const AZStd::string& filePath) + virtual void OnFileUpdate(const Configs::FileSaveSettings& config) { } /** * @brief Called when a file is successfully saved. - * @param filePath The path of the saved file. + * @param config File Save Settings Configuration. */ - virtual void OnFileSaved(const AZStd::string& filePath) + virtual void OnFileSaved(const Configs::FileSaveSettings& config) { } /** * @brief Called when the profiling process starts. - * @param config The configuration settings used for the profiling session. + * @param recordConfig The configuration settings used for the record session. + * @param precisionConfig The configuration settings used for the precision. + * @param debugConfig The configuration settings used for the debugging. */ - virtual void OnProfileStart(const Configs::FileSaveSettings& config) + virtual void OnProfileStart( + const Configs::RecordSettings& recordConfig, + const Configs::PrecisionSettings& precisionConfig, + const Configs::DebugSettings& debugConfig) { } /** * @brief Called when the profiling data is reset. - * @param config The configuration settings used for the profiling session. + * @param recordConfig The configuration settings used for the record session. + * @param precisionConfig The configuration settings used for the precision. */ - virtual void OnProfileReset(const Configs::FileSaveSettings& config) + virtual void OnProfileReset(const Configs::RecordSettings& recordConfig, const Configs::PrecisionSettings& precisionConfig) { } /** * @brief Called when the profiling process stops. - * @param config The configuration settings used for the profiling session. + * @param saveConfig The configuration settings used for the file operations. + * @param recordConfig The configuration settings used for the record session. + * @param precisionConfig The configuration settings used for the precision. + * @param debugConfig The configuration settings used for the debugging. */ - virtual void OnProfileStop(const Configs::FileSaveSettings& config) + virtual void OnProfileStop( + const Configs::FileSaveSettings& saveConfig, + const Configs::RecordSettings& recordConfig, + const Configs::PrecisionSettings& precisionConfig, + const Configs::DebugSettings& debugConfig) { } }; @@ -213,34 +226,41 @@ namespace FPSProfiler OnProfileReset, OnProfileStop); - void OnFileCreated(const AZStd::string& filePath) override + void OnFileCreated(const Configs::FileSaveSettings& config) override { - Call(FN_OnFileCreated, filePath); + Call(FN_OnFileCreated, config); } - void OnFileUpdate(const AZStd::string& filePath) override + void OnFileUpdate(const Configs::FileSaveSettings& config) override { - Call(FN_OnFileUpdate, filePath); + Call(FN_OnFileUpdate, config); } - void OnFileSaved(const AZStd::string& filePath) override + void OnFileSaved(const Configs::FileSaveSettings& config) override { - Call(FN_OnFileSaved, filePath); + Call(FN_OnFileSaved, config); } - void OnProfileStart(const Configs::FileSaveSettings& config) override + void OnProfileStart( + const Configs::RecordSettings& recordConfig, + const Configs::PrecisionSettings& precisionConfig, + const Configs::DebugSettings& debugConfig) override { - Call(FN_OnProfileStart, config); + Call(FN_OnProfileStart, recordConfig, precisionConfig, debugConfig); } - void OnProfileReset(const Configs::FileSaveSettings& config) override + void OnProfileReset(const Configs::RecordSettings& recordConfig, const Configs::PrecisionSettings& precisionConfig) override { - Call(FN_OnProfileReset, config); + Call(FN_OnProfileReset, recordConfig, precisionConfig); } - void OnProfileStop(const Configs::FileSaveSettings& config) override + void OnProfileStop( + const Configs::FileSaveSettings& saveConfig, + const Configs::RecordSettings& recordConfig, + const Configs::PrecisionSettings& precisionConfig, + const Configs::DebugSettings& debugConfig) override { - Call(FN_OnProfileStop, config); + Call(FN_OnProfileStop, saveConfig, recordConfig, precisionConfig, debugConfig); } }; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp index b0bcc9e8..e3888181 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp @@ -144,7 +144,7 @@ namespace FPSProfiler } // Notify - File Saved - FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileSaved, m_configFile.m_OutputFilename.c_str()); + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileSaved, m_configFile); FPSProfilerRequestBus::Handler::BusDisconnect(); } @@ -255,7 +255,7 @@ namespace FPSProfiler } // Notify - Profile Started - FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileStart, m_configFile); + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileStart, m_configRecord, m_configPrecision, m_configDebug); AZ_Printf("FPS Profiler", "Profiling started."); } @@ -277,7 +277,8 @@ namespace FPSProfiler SaveLogToFile(); // Notify - Profile Stopped - FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileStop, m_configFile); + FPSProfilerNotificationBus::Broadcast( + &FPSProfilerNotifications::OnProfileStop, m_configFile, m_configRecord, m_configPrecision, m_configDebug); AZ_Printf("FPS Profiler", "Profiling stopped."); } @@ -294,7 +295,7 @@ namespace FPSProfiler m_logBuffer.clear(); // Notify - Profile Reset - FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileReset, m_configFile); + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileReset, m_configRecord, m_configPrecision); AZ_Printf("FPS Profiler", "Profiling data reseted."); } @@ -483,7 +484,7 @@ namespace FPSProfiler file.Close(); // Notify - File Created - FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileCreated, m_configFile.m_OutputFilename.c_str()); + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileCreated, m_configFile); } void FPSProfilerComponent::WriteDataToFile() @@ -507,7 +508,7 @@ namespace FPSProfiler m_logBuffer.clear(); // Notify - File Update - FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileUpdate, m_configFile.m_OutputFilename.c_str()); + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileUpdate, m_configFile); } float FPSProfilerComponent::BytesToMB(AZStd::size_t bytes) diff --git a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp index d1fa22bc..840a0e51 100644 --- a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp @@ -1,5 +1,6 @@ #include "FPSProfilerConfig.h" +#include #include namespace FPSProfiler::Configs @@ -63,6 +64,22 @@ namespace FPSProfiler::Configs "seconds. This allows you to save automatically without manual input each time."); } } + + if (auto behaviorContext = azrtti_cast(context)) + { + if (behaviorContext->m_classes.contains("FileSaveSettings")) + { + return; + } + + behaviorContext->Class("FileSaveSettings") + ->Attribute(AZ::Script::Attributes::Category, "FPSProfiler") + ->Constructor<>() + ->Property("outputFilename", BehaviorValueProperty(&FileSaveSettings::m_OutputFilename)) + ->Property("autoSave", BehaviorValueProperty(&FileSaveSettings::m_AutoSave)) + ->Property("autoSaveAtFrame", BehaviorValueProperty(&FileSaveSettings::m_AutoSaveAtFrame)) + ->Property("saveWithTimestamp", BehaviorValueProperty(&FileSaveSettings::m_SaveWithTimestamp)); + } } void RecordSettings::Reflect(AZ::ReflectContext* context) @@ -132,6 +149,22 @@ namespace FPSProfiler::Configs ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree); } } + + if (auto behaviorContext = azrtti_cast(context)) + { + if (behaviorContext->m_classes.contains("RecordSettings")) + { + return; + } + + behaviorContext->Class("RecordSettings") + ->Attribute(AZ::Script::Attributes::Category, "FPSProfiler") + ->Constructor<>() + ->Property("recordType", BehaviorValueProperty(&RecordSettings::m_recordType)) + ->Property("framesToSkip", BehaviorValueProperty(&RecordSettings::m_framesToSkip)) + ->Property("framesToRecord", BehaviorValueProperty(&RecordSettings::m_framesToRecord)) + ->Property("recordStats", BehaviorValueProperty(&RecordSettings::m_RecordStats)); + } } void PrecisionSettings::Reflect(AZ::ReflectContext* context) @@ -194,6 +227,22 @@ namespace FPSProfiler::Configs "enabled."); } } + + if (auto* behaviorContext = azrtti_cast(context)) + { + if (behaviorContext->m_classes.contains("PrecisionSettings")) + { + return; + } + + behaviorContext->Class("PrecisionSettings") + ->Attribute(AZ::Script::Attributes::Category, "FPSProfiler") + ->Constructor<>() + ->Property("NearZeroPrecision", BehaviorValueProperty(&PrecisionSettings::m_NearZeroPrecision)) + ->Property("AvgFpsType", BehaviorValueProperty(&PrecisionSettings::m_avgFpsType)) + ->Property("SmoothingFactor", BehaviorValueProperty(&PrecisionSettings::m_smoothingFactor)) + ->Property("KeepHistory", BehaviorValueProperty(&PrecisionSettings::m_keepHistory)); + } } void DebugSettings::Reflect(AZ::ReflectContext* context) @@ -236,5 +285,20 @@ namespace FPSProfiler::Configs }); } } + + if (auto* behaviorContext = azrtti_cast(context)) + { + if (behaviorContext->m_classes.contains("DebugSettings")) + { + return; + } + + behaviorContext->Class("DebugSettings") + ->Attribute(AZ::Script::Attributes::Category, "FPSProfiler") + ->Constructor<>() + ->Property("PrintDebugInfo", BehaviorValueProperty(&DebugSettings::m_PrintDebugInfo)) + ->Property("ShowFps", BehaviorValueProperty(&DebugSettings::m_ShowFps)) + ->Property("Color", BehaviorValueProperty(&DebugSettings::m_Color)); + } } } // namespace FPSProfiler::Configs \ No newline at end of file From c3074cfb80b4e92444fbb1eeaf85ca0c9e26bb22 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Tue, 18 Mar 2025 11:28:39 +0100 Subject: [PATCH 156/175] readme update | fix behaviour reflect Signed-off-by: Wojciech Czerski --- .../Configurations/FPSProfilerConfig.cpp | 36 +++++------ readme.md | 63 +++---------------- 2 files changed, 27 insertions(+), 72 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp index 840a0e51..abbf9bf0 100644 --- a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp @@ -75,10 +75,10 @@ namespace FPSProfiler::Configs behaviorContext->Class("FileSaveSettings") ->Attribute(AZ::Script::Attributes::Category, "FPSProfiler") ->Constructor<>() - ->Property("outputFilename", BehaviorValueProperty(&FileSaveSettings::m_OutputFilename)) - ->Property("autoSave", BehaviorValueProperty(&FileSaveSettings::m_AutoSave)) - ->Property("autoSaveAtFrame", BehaviorValueProperty(&FileSaveSettings::m_AutoSaveAtFrame)) - ->Property("saveWithTimestamp", BehaviorValueProperty(&FileSaveSettings::m_SaveWithTimestamp)); + ->Property("m_OutputFilename", BehaviorValueProperty(&FileSaveSettings::m_OutputFilename)) + ->Property("m_AutoSave", BehaviorValueProperty(&FileSaveSettings::m_AutoSave)) + ->Property("m_AutoSaveAtFrame", BehaviorValueProperty(&FileSaveSettings::m_AutoSaveAtFrame)) + ->Property("m_SaveWithTimestamp", BehaviorValueProperty(&FileSaveSettings::m_SaveWithTimestamp)); } } @@ -160,10 +160,10 @@ namespace FPSProfiler::Configs behaviorContext->Class("RecordSettings") ->Attribute(AZ::Script::Attributes::Category, "FPSProfiler") ->Constructor<>() - ->Property("recordType", BehaviorValueProperty(&RecordSettings::m_recordType)) - ->Property("framesToSkip", BehaviorValueProperty(&RecordSettings::m_framesToSkip)) - ->Property("framesToRecord", BehaviorValueProperty(&RecordSettings::m_framesToRecord)) - ->Property("recordStats", BehaviorValueProperty(&RecordSettings::m_RecordStats)); + ->Property("m_recordType", BehaviorValueProperty(&RecordSettings::m_recordType)) + ->Property("m_framesToSkip", BehaviorValueProperty(&RecordSettings::m_framesToSkip)) + ->Property("m_framesToRecord", BehaviorValueProperty(&RecordSettings::m_framesToRecord)) + ->Property("m_RecordStats", BehaviorValueProperty(&RecordSettings::m_RecordStats)); } } @@ -238,10 +238,10 @@ namespace FPSProfiler::Configs behaviorContext->Class("PrecisionSettings") ->Attribute(AZ::Script::Attributes::Category, "FPSProfiler") ->Constructor<>() - ->Property("NearZeroPrecision", BehaviorValueProperty(&PrecisionSettings::m_NearZeroPrecision)) - ->Property("AvgFpsType", BehaviorValueProperty(&PrecisionSettings::m_avgFpsType)) - ->Property("SmoothingFactor", BehaviorValueProperty(&PrecisionSettings::m_smoothingFactor)) - ->Property("KeepHistory", BehaviorValueProperty(&PrecisionSettings::m_keepHistory)); + ->Property("m_NearZeroPrecision", BehaviorValueProperty(&PrecisionSettings::m_NearZeroPrecision)) + ->Property("m_avgFpsType", BehaviorValueProperty(&PrecisionSettings::m_avgFpsType)) + ->Property("m_smoothingFactor", BehaviorValueProperty(&PrecisionSettings::m_smoothingFactor)) + ->Property("m_keepHistory", BehaviorValueProperty(&PrecisionSettings::m_keepHistory)); } } @@ -256,9 +256,9 @@ namespace FPSProfiler::Configs serializeContext->Class() ->Version(0) - ->Field("PrintDebugInfo", &DebugSettings::m_PrintDebugInfo) - ->Field("ShowFps", &DebugSettings::m_ShowFps) - ->Field("Color", &DebugSettings::m_Color); + ->Field("m_PrintDebugInfo", &DebugSettings::m_PrintDebugInfo) + ->Field("m_ShowFps", &DebugSettings::m_ShowFps) + ->Field("m_Color", &DebugSettings::m_Color); if (auto* editContext = serializeContext->GetEditContext()) { @@ -296,9 +296,9 @@ namespace FPSProfiler::Configs behaviorContext->Class("DebugSettings") ->Attribute(AZ::Script::Attributes::Category, "FPSProfiler") ->Constructor<>() - ->Property("PrintDebugInfo", BehaviorValueProperty(&DebugSettings::m_PrintDebugInfo)) - ->Property("ShowFps", BehaviorValueProperty(&DebugSettings::m_ShowFps)) - ->Property("Color", BehaviorValueProperty(&DebugSettings::m_Color)); + ->Property("m_PrintDebugInfo", BehaviorValueProperty(&DebugSettings::m_PrintDebugInfo)) + ->Property("m_ShowFps", BehaviorValueProperty(&DebugSettings::m_ShowFps)) + ->Property("m_Color", BehaviorValueProperty(&DebugSettings::m_Color)); } } } // namespace FPSProfiler::Configs \ No newline at end of file diff --git a/readme.md b/readme.md index 3d254bf4..daa21406 100644 --- a/readme.md +++ b/readme.md @@ -421,65 +421,20 @@ void OnProfileStart(const Configs::FileSaveSettings& config) override ``` ### In Lua -```shell --- Table to hold our script functions -local profilerScript = {} - --- Function called when profiling starts -function profilerScript:OnProfileStart(config) - Debug.Log("Profiling started. Stopping in 60 seconds...") - - -- Start a 60-second timer before stopping profiling - self:StartTimer(60, function() - Debug.Log("Stopping profiling now...") - FPSProfilerRequestBus.Broadcast.StopProfiling() - end) -end - --- Function to start a timer -function profilerScript:StartTimer(delay, callback) - if self.timerEventId then - -- Prevent multiple timers from stacking - TickBus.Disconnect(self, self.timerEventId) - end - - self.timerTimeRemaining = delay - self.timerCallback = callback - self.timerEventId = TickBus.Connect(self) -end +```lua +FPSProfilerHandler = {} --- Tick event to track time -function profilerScript:OnTick(deltaTime, timePoint) - if self.timerTimeRemaining then - self.timerTimeRemaining = self.timerTimeRemaining - deltaTime - if self.timerTimeRemaining <= 0 then - -- Time is up, trigger callback and disconnect - if self.timerCallback then - self.timerCallback() - end - TickBus.Disconnect(self, self.timerEventId) - self.timerEventId = nil - end - end +-- Called when a new file is created +function FPSProfilerHandler:OnFileCreated(config) + Debug.Log("File Created: " .. config.m_OutputFilename) end --- Register as an FPSProfilerNotificationBus listener -function profilerScript:OnActivate() - FPSProfilerNotificationBus.Connect(self) -end - --- Cleanup when script is deactivated -function profilerScript:OnDeactivate() - FPSProfilerNotificationBus.Disconnect(self) - - -- Disconnect timer if still active - if self.timerEventId then - TickBus.Disconnect(self, self.timerEventId) - end +function FPSProfilerHandler:OnActivate() + -- Connect the handler to listen for notifications + FPSProfilerNotificationBus.Connect(FPSProfilerHandler) end --- Return the table so O3DE can use it -return profilerScript +return FPSProfilerHandler ``` ### In Script Canvas From 2fb02a7774f07ab7abb9d7a6132e677c8b587b8c Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 3 Apr 2025 14:06:18 +0200 Subject: [PATCH 157/175] fix formatting Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake index fe67fb56..b42e63b2 100644 --- a/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake +++ b/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake @@ -2,8 +2,8 @@ set(FILES Source/FPSProfilerModuleInterface.cpp Source/FPSProfilerModuleInterface.h - Source/Clients/FPSProfilerComponent.cpp - Source/Clients/FPSProfilerComponent.h + Source/Clients/FPSProfilerComponent.cpp + Source/Clients/FPSProfilerComponent.h Source/Configurations/FPSProfilerConfig.cpp Source/Configurations/FPSProfilerConfig.h ) From 70db2692e191d28988de670af5676e0d450edcc4 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 3 Apr 2025 14:07:00 +0200 Subject: [PATCH 158/175] fix formatting cmake Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake index 4c599923..11417025 100644 --- a/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake +++ b/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake @@ -1,5 +1,5 @@ set(FILES - Source/Tools/FPSProfilerEditorComponent.cpp - Source/Tools/FPSProfilerEditorComponent.h + Source/Tools/FPSProfilerEditorComponent.cpp + Source/Tools/FPSProfilerEditorComponent.h ) From 35f6a20016222755216415a81e33431162a3b564 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 3 Apr 2025 14:10:21 +0200 Subject: [PATCH 159/175] add missing new line Signed-off-by: Wojciech Czerski --- .../Code/Source/Configurations/FPSProfilerConfig.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp index abbf9bf0..d69d01c8 100644 --- a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp @@ -301,4 +301,4 @@ namespace FPSProfiler::Configs ->Property("m_Color", BehaviorValueProperty(&DebugSettings::m_Color)); } } -} // namespace FPSProfiler::Configs \ No newline at end of file +} // namespace FPSProfiler::Configs From aa875b12077afe9c1a72e681d187d41c83b525e6 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 3 Apr 2025 14:34:04 +0200 Subject: [PATCH 160/175] ediotr context for component Signed-off-by: Wojciech Czerski --- .../Code/Source/Clients/FPSProfilerComponent.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp index e3888181..5d90520c 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -24,6 +25,20 @@ namespace FPSProfiler ->Field("m_configRecord", &FPSProfilerComponent::m_configRecord) ->Field("m_configPrecision", &FPSProfilerComponent::m_configPrecision) ->Field("m_configDebug", &FPSProfilerComponent::m_configDebug); + + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + editContext->Class("FPS Profiler", "Tracks FPS, GPU and CPU performance and saves it into .csv") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Category, "Performance") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerComponent::m_configFile) + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerComponent::m_configRecord) + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerComponent::m_configPrecision) + ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerComponent::m_configDebug); + } } // EBus Reflect for Lua and Script Canvas From ab86e28eec2b408594d20c473411d2cb8e739492 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 3 Apr 2025 14:36:18 +0200 Subject: [PATCH 161/175] remove editor source files from cmake Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake | 2 -- Gems/FPSProfiler/Code/fpsprofiler_editor_shared_files.cmake | 1 - Gems/FPSProfiler/Code/fpsprofiler_editor_tests_files.cmake | 1 - 3 files changed, 4 deletions(-) diff --git a/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake index 11417025..f5526eeb 100644 --- a/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake +++ b/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake @@ -1,5 +1,3 @@ set(FILES - Source/Tools/FPSProfilerEditorComponent.cpp - Source/Tools/FPSProfilerEditorComponent.h ) diff --git a/Gems/FPSProfiler/Code/fpsprofiler_editor_shared_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_editor_shared_files.cmake index af1d259a..f5526eeb 100644 --- a/Gems/FPSProfiler/Code/fpsprofiler_editor_shared_files.cmake +++ b/Gems/FPSProfiler/Code/fpsprofiler_editor_shared_files.cmake @@ -1,4 +1,3 @@ set(FILES - Source/Tools/FPSProfilerEditorModule.cpp ) diff --git a/Gems/FPSProfiler/Code/fpsprofiler_editor_tests_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_editor_tests_files.cmake index 341daf50..f5526eeb 100644 --- a/Gems/FPSProfiler/Code/fpsprofiler_editor_tests_files.cmake +++ b/Gems/FPSProfiler/Code/fpsprofiler_editor_tests_files.cmake @@ -1,4 +1,3 @@ set(FILES - Tests/Tools/FPSProfilerEditorTest.cpp ) From c1abc2dbdf966a56ed12ba58c7fdff0c83d982b9 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 3 Apr 2025 14:45:04 +0200 Subject: [PATCH 162/175] remove ediotr component Signed-off-by: Wojciech Czerski --- .../Tools/FPSProfilerEditorComponent.cpp | 95 ------------------- .../Source/Tools/FPSProfilerEditorComponent.h | 37 -------- .../Source/Tools/FPSProfilerEditorModule.cpp | 6 +- .../fpsprofiler_editor_shared_files.cmake | 1 + 4 files changed, 4 insertions(+), 135 deletions(-) delete mode 100644 Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp delete mode 100644 Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.h diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp deleted file mode 100644 index 213c2352..00000000 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.cpp +++ /dev/null @@ -1,95 +0,0 @@ -#include "FPSProfilerEditorComponent.h" -#include - -#include -#include -#include -#include - -namespace FPSProfiler -{ - void FPSProfilerEditorComponent::Reflect(AZ::ReflectContext* context) - { - Configs::FileSaveSettings::Reflect(context); - Configs::RecordSettings::Reflect(context); - Configs::PrecisionSettings::Reflect(context); - Configs::DebugSettings::Reflect(context); - - if (auto serializeContext = azrtti_cast(context)) - { - serializeContext->Class() - ->Version(0) - ->Field("m_configFileEditor", &FPSProfilerEditorComponent::m_configFile) - ->Field("m_configRecordEditor", &FPSProfilerEditorComponent::m_configRecord) - ->Field("m_configPrecisionEditor", &FPSProfilerEditorComponent::m_configPrecision) - ->Field("m_configDebugEditor", &FPSProfilerEditorComponent::m_configDebug); - - if (AZ::EditContext* editContext = serializeContext->GetEditContext()) - { - editContext->Class("FPS Profiler", "Tracks FPS, GPU and CPU performance and saves it into .csv") - ->ClassElement(AZ::Edit::ClassElements::EditorData, "") - ->Attribute(AZ::Edit::Attributes::Category, "Performance") - ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Level")) - ->Attribute(AZ::Edit::Attributes::AutoExpand, true) - - ->UIElement(AZ::Edit::UIHandlers::Button, "", "Click to open file dialog.") - ->Attribute(AZ::Edit::Attributes::ButtonText, "Select Csv File Path") - ->Attribute(AZ::Edit::Attributes::ChangeNotify, &FPSProfilerEditorComponent::SelectCsvPath) - - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorComponent::m_configFile) - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorComponent::m_configRecord) - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorComponent::m_configPrecision) - ->DataElement(AZ::Edit::UIHandlers::Default, &FPSProfilerEditorComponent::m_configDebug); - } - } - } - - void FPSProfilerEditorComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) - { - provided.push_back(AZ_CRC_CE("FPSProfilerEditorService")); - } - - void FPSProfilerEditorComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) - { - incompatible.push_back(AZ_CRC_CE("FPSProfilerEditorService")); - } - - void FPSProfilerEditorComponent::Activate() - { - } - - void FPSProfilerEditorComponent::Deactivate() - { - } - - void FPSProfilerEditorComponent::BuildGameEntity(AZ::Entity* entity) - { - if (FPSProfilerComponent* gameComponent = entity->CreateComponent()) - { - gameComponent->m_configFile = m_configFile; - gameComponent->m_configRecord = m_configRecord; - gameComponent->m_configPrecision = m_configPrecision; - gameComponent->m_configDebug = m_configDebug; - } - } - - AZ::u32 FPSProfilerEditorComponent::SelectCsvPath() - { - QString fileName = QFileDialog::getSaveFileName(AzToolsFramework::GetActiveWindow(), "Pick a csv file path.", "", "*.csv"); - - if (fileName.isEmpty()) - { - QMessageBox::warning(AzToolsFramework::GetActiveWindow(), "Error", "Please specify file path!", QMessageBox::Ok); - return AZ::Edit::PropertyRefreshLevels::None; - } - - // Ensure the file has the .csv extension - if (!fileName.endsWith(".csv", Qt::CaseInsensitive)) - { - fileName += ".csv"; // Auto-append .csv if missing - } - - m_configFile.m_OutputFilename = AZStd::string(fileName.toUtf8().constData()); - return AZ::Edit::PropertyRefreshLevels::EntireTree; - } -} // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.h b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.h deleted file mode 100644 index 23dd0773..00000000 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorComponent.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include - -#include -#include - -namespace FPSProfiler -{ - /// System component for FPSProfiler editor - class FPSProfilerEditorComponent : public AzToolsFramework::Components::EditorComponentBase - { - public: - AZ_EDITOR_COMPONENT(FPSProfilerEditorComponent, FPSProfilerEditorComponentTypeId, EditorComponentBase); - - static void Reflect(AZ::ReflectContext* context); - - FPSProfilerEditorComponent() = default; - ~FPSProfilerEditorComponent() override = default; - - // AZ::Component - void Activate() override; - void Deactivate() override; - void BuildGameEntity(AZ::Entity*) override; - - private: - static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); - static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); - - Configs::FileSaveSettings m_configFile; //!< Stores editor settings for the profiler - Configs::RecordSettings m_configRecord; //!< Stores editor settings for the profiler - Configs::PrecisionSettings m_configPrecision; //!< Stores editor settings for the profiler - Configs::DebugSettings m_configDebug; //!< Stores editor settings for the profiler - - AZ::u32 SelectCsvPath(); - }; -} // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp index 7e919588..2d8ccc9b 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp @@ -1,5 +1,5 @@ +#include "Clients/FPSProfilerComponent.h" -#include "FPSProfilerEditorComponent.h" #include #include @@ -20,7 +20,7 @@ namespace FPSProfiler m_descriptors.insert( m_descriptors.end(), { - FPSProfilerEditorComponent::CreateDescriptor(), + FPSProfilerComponent::CreateDescriptor(), }); } @@ -31,7 +31,7 @@ namespace FPSProfiler AZ::ComponentTypeList GetRequiredSystemComponents() const override { return AZ::ComponentTypeList{ - azrtti_typeid(), + azrtti_typeid(), }; } }; diff --git a/Gems/FPSProfiler/Code/fpsprofiler_editor_shared_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_editor_shared_files.cmake index f5526eeb..af1d259a 100644 --- a/Gems/FPSProfiler/Code/fpsprofiler_editor_shared_files.cmake +++ b/Gems/FPSProfiler/Code/fpsprofiler_editor_shared_files.cmake @@ -1,3 +1,4 @@ set(FILES + Source/Tools/FPSProfilerEditorModule.cpp ) From c970d6d26131a2eeb6a14aaa260c3ccd312382c8 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 3 Apr 2025 14:52:08 +0200 Subject: [PATCH 163/175] remove editor component depencencies Signed-off-by: Wojciech Czerski --- .../Code/Source/FPSProfilerModuleInterface.cpp | 7 ------- .../Code/Source/FPSProfilerModuleInterface.h | 5 ----- .../Code/Source/Tools/FPSProfilerEditorModule.cpp | 11 ----------- 3 files changed, 23 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp index 968f1808..b5049c81 100644 --- a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp +++ b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp @@ -24,11 +24,4 @@ namespace FPSProfiler FPSProfilerComponent::CreateDescriptor(), }); } - - AZ::ComponentTypeList FPSProfilerModuleInterface::GetRequiredSystemComponents() const - { - return AZ::ComponentTypeList{ - azrtti_typeid(), - }; - } } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.h b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.h index 6595088e..ee0f505a 100644 --- a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.h +++ b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.h @@ -14,10 +14,5 @@ namespace FPSProfiler AZ_CLASS_ALLOCATOR_DECL FPSProfilerModuleInterface(); - - /** - * Add required SystemComponents to the SystemEntity. - */ - AZ::ComponentTypeList GetRequiredSystemComponents() const override; }; } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp index 2d8ccc9b..b17623c3 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp @@ -23,17 +23,6 @@ namespace FPSProfiler FPSProfilerComponent::CreateDescriptor(), }); } - - /** - * Add required SystemComponents to the SystemEntity. - * Non-SystemComponents should not be added here - */ - AZ::ComponentTypeList GetRequiredSystemComponents() const override - { - return AZ::ComponentTypeList{ - azrtti_typeid(), - }; - } }; } // namespace FPSProfiler From 8d2d912fc38e5cde39ebddfe354ddc7db865688b Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 3 Apr 2025 14:54:05 +0200 Subject: [PATCH 164/175] remove editor component type id Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h | 1 - 1 file changed, 1 deletion(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h index bef5c3ea..916e72b9 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h @@ -5,7 +5,6 @@ namespace FPSProfiler { // System Component TypeIds inline constexpr const char* FPSProfilerComponentTypeId = "{B11CE88E-E1C5-404F-83D2-0D3850445A13}"; - inline constexpr const char* FPSProfilerEditorComponentTypeId = "{F4308920-CD0B-4A2E-91DE-2EC1E970F97A}"; // Configs TypeIds inline constexpr const char* FPSProfilerConfigFileTypeId = "{68627A89-9426-4640-B460-63E6AA42CFBC}"; From 15b1771e45ae10344a6ea5683690d86cae4dd987 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 3 Apr 2025 15:06:21 +0200 Subject: [PATCH 165/175] change path string into AZ::IO::Path Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 6 ++-- .../Source/Clients/FPSProfilerComponent.cpp | 29 +++++++++---------- .../Source/Clients/FPSProfilerComponent.h | 8 ++--- .../Source/Configurations/FPSProfilerConfig.h | 2 +- 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 78ba601e..5be9c9a9 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -52,14 +52,14 @@ namespace FPSProfiler * @warning This function is NOT runtime safe. Use @ref SafeChangeSavePath instead. * @param newSavePath The new file path where profiling data should be saved. */ - virtual void ChangeSavePath(const AZStd::string& newSavePath) = 0; + virtual void ChangeSavePath(const AZ::IO::Path& newSavePath) = 0; /** * @brief Safely changes the save path during runtime. * This method stops profiling, saves the current data, and then updates the path. * @param newSavePath The new file path where profiling data should be saved. */ - virtual void SafeChangeSavePath(const AZStd::string& newSavePath) = 0; + virtual void SafeChangeSavePath(const AZ::IO::Path& newSavePath) = 0; /** * @brief Retrieves the minimum recorded FPS during the profiling session. @@ -107,7 +107,7 @@ namespace FPSProfiler * @param newSavePath The csv file path where the log should be saved. * @param useSafeChangePath If true, the function will use @ref SafeChangeSavePath to ensure runtime safety. */ - virtual void SaveLogToFileWithNewPath(const AZStd::string& newSavePath, bool useSafeChangePath) = 0; + virtual void SaveLogToFileWithNewPath(const AZ::IO::Path& newSavePath, bool useSafeChangePath) = 0; /** * @brief Enables or disables FPS display on-screen. diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp index 5d90520c..b24c035c 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp @@ -324,7 +324,7 @@ namespace FPSProfiler return m_configRecord.m_RecordStats != Configs::RecordStatistics::None; } - void FPSProfilerComponent::ChangeSavePath(const AZStd::string& newSavePath) + void FPSProfilerComponent::ChangeSavePath(const AZ::IO::Path& newSavePath) { if (!IsPathValid(newSavePath)) { @@ -335,7 +335,7 @@ namespace FPSProfiler AZ_Warning("FPS Profiler", !m_configDebug.m_PrintDebugInfo && !m_isProfiling, "Path changed during activated profiling."); } - void FPSProfilerComponent::SafeChangeSavePath(const AZStd::string& newSavePath) + void FPSProfilerComponent::SafeChangeSavePath(const AZ::IO::Path& newSavePath) { // If profiling is enabled, save current opened file and stop profiling. StopProfiling(); @@ -393,7 +393,7 @@ namespace FPSProfiler WriteDataToFile(); } - void FPSProfilerComponent::SaveLogToFileWithNewPath(const AZStd::string& newSavePath, bool useSafeChangePath) + void FPSProfilerComponent::SaveLogToFileWithNewPath(const AZ::IO::Path& newSavePath, bool useSafeChangePath) { if (useSafeChangePath) { @@ -485,10 +485,9 @@ namespace FPSProfiler char timestamp[20]; strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", &timeInfo); - AZ::IO::Path logFilePath(m_configFile.m_OutputFilename); - logFilePath.ReplaceFilename((logFilePath.Stem().String() + "_" + timestamp + logFilePath.Extension().String()).data()); - - m_configFile.m_OutputFilename = logFilePath.c_str(); + m_configFile.m_OutputFilename.ReplaceFilename( + (m_configFile.m_OutputFilename.Stem().String() + "_" + timestamp + m_configFile.m_OutputFilename.Extension().String()) + .data()); } // Write profiling headers to file @@ -531,19 +530,17 @@ namespace FPSProfiler return static_cast(bytes) / (1024.0f * 1024.0f); } - bool FPSProfilerComponent::IsPathValid(const AZStd::string& path) const + bool FPSProfilerComponent::IsPathValid(const AZ::IO::Path& path) const { AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); - AZ::IO::Path pathToValidate(path.c_str()); - if (pathToValidate.empty() || !pathToValidate.HasFilename() || !pathToValidate.HasExtension() || !fileIO || - !fileIO->ResolvePath(pathToValidate)) + if (path.empty() || !path.HasFilename() || !path.HasExtension() || !fileIO || !fileIO->ResolvePath(path)) { - const char* reason = pathToValidate.empty() ? "Path cannot be empty." - : !pathToValidate.HasFilename() ? "Path must have a file at the end." - : !pathToValidate.HasExtension() ? "Path must have a *.csv extension." - : !fileIO ? "Could not get a FileIO object. Try again." - : "Path is not registered or recognizable by O3DE FileIO System."; + const char* reason = path.empty() ? "Path cannot be empty." + : !path.HasFilename() ? "Path must have a file at the end." + : !path.HasExtension() ? "Path must have a *.csv extension." + : !fileIO ? "Could not get a FileIO object. Try again." + : "Path is not registered or recognizable by O3DE FileIO System."; AZ_Warning("FPSProfiler::IsPathValid", !m_configDebug.m_PrintDebugInfo, "%s", reason); return false; diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h index c63f64ed..715e2633 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h @@ -45,8 +45,8 @@ namespace FPSProfiler void ResetProfilingData() override; [[nodiscard]] bool IsProfiling() const override; [[nodiscard]] bool IsAnySaveOptionEnabled() const override; - void ChangeSavePath(const AZStd::string& newSavePath) override; - void SafeChangeSavePath(const AZStd::string& newSavePath) override; + void ChangeSavePath(const AZ::IO::Path& newSavePath) override; + void SafeChangeSavePath(const AZ::IO::Path& newSavePath) override; [[nodiscard]] float GetMinFps() const override; [[nodiscard]] float GetMaxFps() const override; [[nodiscard]] float GetAvgFps() const override; @@ -54,7 +54,7 @@ namespace FPSProfiler [[nodiscard]] AZStd::pair GetCpuMemoryUsed() const override; [[nodiscard]] AZStd::pair GetGpuMemoryUsed() const override; void SaveLogToFile() override; - void SaveLogToFileWithNewPath(const AZStd::string& newSavePath, bool useSafeChangePath) override; + void SaveLogToFileWithNewPath(const AZ::IO::Path& newSavePath, bool useSafeChangePath) override; void ShowFpsOnScreen(bool enable) override; public: @@ -88,7 +88,7 @@ namespace FPSProfiler // Utility Functions void CalculateFpsData(const float& deltaTime); static float BytesToMB(AZStd::size_t bytes); - [[nodiscard]] bool IsPathValid(const AZStd::string& path) const; + [[nodiscard]] bool IsPathValid(const AZ::IO::Path& path) const; // Debug Display void ShowFps() const; diff --git a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.h index 4cbe7c33..aa5b4643 100644 --- a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.h +++ b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.h @@ -40,7 +40,7 @@ namespace FPSProfiler::Configs AZ_TYPE_INFO(FileSaveSettings, FPSProfilerConfigFileTypeId); static void Reflect(AZ::ReflectContext* context); - AZStd::string m_OutputFilename = "@user@/fps_log.csv"; + AZ::IO::Path m_OutputFilename = "@user@/fps_log.csv"; bool m_AutoSave = true; int m_AutoSaveAtFrame = 100; bool m_SaveWithTimestamp = true; From ca305001ef6f57ff5334720e428c556ee2313caa Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 3 Apr 2025 15:11:35 +0200 Subject: [PATCH 166/175] remove unecessary checks for config refelections Signed-off-by: Wojciech Czerski --- .../Configurations/FPSProfilerConfig.cpp | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp index d69d01c8..77536d47 100644 --- a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp @@ -9,11 +9,6 @@ namespace FPSProfiler::Configs { if (auto serializeContext = azrtti_cast(context)) { - if (serializeContext->FindClassData(azrtti_typeid())) // Prevent duplicate registration - { - return; - } - serializeContext->Class() ->Version(0) ->Field("m_OutputFilename", &FileSaveSettings::m_OutputFilename) @@ -86,11 +81,6 @@ namespace FPSProfiler::Configs { if (auto serializeContext = azrtti_cast(context)) { - if (serializeContext->FindClassData(azrtti_typeid())) // Prevent duplicate registration - { - return; - } - serializeContext->Class() ->Version(0) ->Field("m_recordType", &RecordSettings::m_recordType) @@ -152,11 +142,6 @@ namespace FPSProfiler::Configs if (auto behaviorContext = azrtti_cast(context)) { - if (behaviorContext->m_classes.contains("RecordSettings")) - { - return; - } - behaviorContext->Class("RecordSettings") ->Attribute(AZ::Script::Attributes::Category, "FPSProfiler") ->Constructor<>() @@ -171,11 +156,6 @@ namespace FPSProfiler::Configs { if (auto* serializeContext = azrtti_cast(context)) { - if (serializeContext->FindClassData(azrtti_typeid())) // Prevent duplicate registration - { - return; - } - serializeContext->Class() ->Version(0) ->Field("m_NearZeroPrecision", &PrecisionSettings::m_NearZeroPrecision) @@ -249,11 +229,6 @@ namespace FPSProfiler::Configs { if (auto* serializeContext = azrtti_cast(context)) { - if (serializeContext->FindClassData(azrtti_typeid())) // Prevent duplicate registration - { - return; - } - serializeContext->Class() ->Version(0) ->Field("m_PrintDebugInfo", &DebugSettings::m_PrintDebugInfo) From e164d0e2227e3a8487fb725c7b18c20510566a80 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 3 Apr 2025 15:13:23 +0200 Subject: [PATCH 167/175] make config private | refactor Signed-off-by: Wojciech Czerski --- .../Source/Clients/FPSProfilerComponent.h | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h index 715e2633..c71a153a 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h @@ -57,14 +57,25 @@ namespace FPSProfiler void SaveLogToFileWithNewPath(const AZ::IO::Path& newSavePath, bool useSafeChangePath) override; void ShowFpsOnScreen(bool enable) override; - public: + private: + // File Operations + void CreateLogFile(); + void WriteDataToFile(); + + // Utility Functions + void CalculateFpsData(const float& deltaTime); + static float BytesToMB(AZStd::size_t bytes); + [[nodiscard]] bool IsPathValid(const AZ::IO::Path& path) const; + + // Debug Display + void ShowFps() const; + // Profiler Configurations Configs::FileSaveSettings m_configFile; //!< Stores editor settings for the profiler Configs::RecordSettings m_configRecord; //!< Stores editor settings for the profiler Configs::PrecisionSettings m_configPrecision; //!< Stores editor settings for the profiler Configs::DebugSettings m_configDebug; //!< Stores editor settings for the profiler - private: // FPS Tracking Data bool m_isProfiling = false; //!< Flag to indicate if profiling is active float m_minFps = 0.0f; //!< Lowest FPS value recorded @@ -80,17 +91,5 @@ namespace FPSProfiler AZStd::vector m_logBuffer; //!< Buffer for log entries, cleared periodically if auto save enabled. static constexpr AZStd::size_t MAX_LOG_BUFFER_SIZE = 1024 * 128; //!< Max log buffer size static constexpr AZStd::size_t MAX_LOG_BUFFER_LINE_SIZE = 128; //!< Max length per log line - - // File Operations - void CreateLogFile(); - void WriteDataToFile(); - - // Utility Functions - void CalculateFpsData(const float& deltaTime); - static float BytesToMB(AZStd::size_t bytes); - [[nodiscard]] bool IsPathValid(const AZ::IO::Path& path) const; - - // Debug Display - void ShowFps() const; }; } // namespace FPSProfiler From a8b8a459736e972b7faea90235ba5d03ee21d2e0 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 3 Apr 2025 15:25:51 +0200 Subject: [PATCH 168/175] remove const from bus notifies - blocking file modifications Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 54 +++++++------------ 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index 5be9c9a9..c883605e 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -140,25 +140,19 @@ namespace FPSProfiler * @brief Called when a new file is created. * @param config File Save Settings Configuration. */ - virtual void OnFileCreated(const Configs::FileSaveSettings& config) - { - } + virtual void OnFileCreated(Configs::FileSaveSettings& config) = 0; /** * @brief Called when an existing file is updated. * @param config File Save Settings Configuration. */ - virtual void OnFileUpdate(const Configs::FileSaveSettings& config) - { - } + virtual void OnFileUpdate(Configs::FileSaveSettings& config) = 0; /** * @brief Called when a file is successfully saved. * @param config File Save Settings Configuration. */ - virtual void OnFileSaved(const Configs::FileSaveSettings& config) - { - } + virtual void OnFileSaved(Configs::FileSaveSettings& config) = 0; /** * @brief Called when the profiling process starts. @@ -167,20 +161,14 @@ namespace FPSProfiler * @param debugConfig The configuration settings used for the debugging. */ virtual void OnProfileStart( - const Configs::RecordSettings& recordConfig, - const Configs::PrecisionSettings& precisionConfig, - const Configs::DebugSettings& debugConfig) - { - } + Configs::RecordSettings& recordConfig, Configs::PrecisionSettings& precisionConfig, Configs::DebugSettings& debugConfig) = 0; /** * @brief Called when the profiling data is reset. * @param recordConfig The configuration settings used for the record session. * @param precisionConfig The configuration settings used for the precision. */ - virtual void OnProfileReset(const Configs::RecordSettings& recordConfig, const Configs::PrecisionSettings& precisionConfig) - { - } + virtual void OnProfileReset(Configs::RecordSettings& recordConfig, Configs::PrecisionSettings& precisionConfig) = 0; /** * @brief Called when the profiling process stops. @@ -190,12 +178,10 @@ namespace FPSProfiler * @param debugConfig The configuration settings used for the debugging. */ virtual void OnProfileStop( - const Configs::FileSaveSettings& saveConfig, - const Configs::RecordSettings& recordConfig, - const Configs::PrecisionSettings& precisionConfig, - const Configs::DebugSettings& debugConfig) - { - } + Configs::FileSaveSettings& saveConfig, + Configs::RecordSettings& recordConfig, + Configs::PrecisionSettings& precisionConfig, + Configs::DebugSettings& debugConfig) = 0; }; class FPSProfilerNotificationBusTraits : public AZ::EBusTraits @@ -226,39 +212,39 @@ namespace FPSProfiler OnProfileReset, OnProfileStop); - void OnFileCreated(const Configs::FileSaveSettings& config) override + void OnFileCreated(Configs::FileSaveSettings& config) override { Call(FN_OnFileCreated, config); } - void OnFileUpdate(const Configs::FileSaveSettings& config) override + void OnFileUpdate(Configs::FileSaveSettings& config) override { Call(FN_OnFileUpdate, config); } - void OnFileSaved(const Configs::FileSaveSettings& config) override + void OnFileSaved(Configs::FileSaveSettings& config) override { Call(FN_OnFileSaved, config); } void OnProfileStart( - const Configs::RecordSettings& recordConfig, - const Configs::PrecisionSettings& precisionConfig, - const Configs::DebugSettings& debugConfig) override + Configs::RecordSettings& recordConfig, + Configs::PrecisionSettings& precisionConfig, + Configs::DebugSettings& debugConfig) override { Call(FN_OnProfileStart, recordConfig, precisionConfig, debugConfig); } - void OnProfileReset(const Configs::RecordSettings& recordConfig, const Configs::PrecisionSettings& precisionConfig) override + void OnProfileReset(Configs::RecordSettings& recordConfig, Configs::PrecisionSettings& precisionConfig) override { Call(FN_OnProfileReset, recordConfig, precisionConfig); } void OnProfileStop( - const Configs::FileSaveSettings& saveConfig, - const Configs::RecordSettings& recordConfig, - const Configs::PrecisionSettings& precisionConfig, - const Configs::DebugSettings& debugConfig) override + Configs::FileSaveSettings& saveConfig, + Configs::RecordSettings& recordConfig, + Configs::PrecisionSettings& precisionConfig, + Configs::DebugSettings& debugConfig) override { Call(FN_OnProfileStop, saveConfig, recordConfig, precisionConfig, debugConfig); } From 3ba5c4b9c5499eb614a98770ddf55de3595e582a Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 3 Apr 2025 15:47:27 +0200 Subject: [PATCH 169/175] fixed fps counter display positioning Signed-off-by: Wojciech Czerski --- Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp index b24c035c..29cba3fb 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp @@ -563,7 +563,7 @@ namespace FPSProfiler debugDisplay->SetColor(AZ::Colors::Red); debugDisplay->SetAlpha(1.0f); - AZStd::string debugText = AZStd::string::format("Profiler | FPS: %.2f", m_currentFps); - debugDisplay->Draw2dTextLabel(10, 10, 1.0f, debugText.c_str(), true); + AZStd::string debugText = AZStd::string::format("FPS: %.2f", m_currentFps); + debugDisplay->Draw2dTextLabel(100, 50, 1.0f, debugText.c_str(), true); } } // namespace FPSProfiler From 99c5e6fe5537f3c196fa17e8debd4dcf2df146f4 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 3 Apr 2025 15:52:23 +0200 Subject: [PATCH 170/175] fix readme conflicts Signed-off-by: Wojciech Czerski --- readme.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index daa21406..9efd845e 100644 --- a/readme.md +++ b/readme.md @@ -47,6 +47,23 @@ Refer to [readme](https://github.com/RobotecAI/robotec-o3de-tools/tree/main/Gems # RobotecSpectatorCamera A component that allows to look at an entity from 3rd person perspective and to switch camera mode to the free flying mode (to switch mode press the `C` key). It also allows to enable/disable following the target's rotation and to add a vertical offset to change the `look at` point of the target entity. +The Spectator camera component can be configured to centre the cursor when moving the camera (this gives the full range of rotation regardless of the available screen space) or to let the cursor move freely on the screen when moving the camera (this reduces the range of rotation, e.g. in third person mode a full rotation may require a few repeats of the (RMB press ->Rotate camera ->RMB release ->Move cursor to previous start position ->Repeat) cycle). By default this option is set to false (cursor is not centered). This option can be configured via the `setreg` file or by passing `--regset` flag in the command line. Example: +- `.setreg`: +```json +{ + "O3DE": + { + "SpectatorCamera": + { + "MoveCursorToTheCenter": false + } + } +} +``` +- `--regset flag`: +``` +./Editor --regset="/O3DE/SpectatorCamera/MoveCursorToTheCenter=true" +``` ![](doc/RobotecSpectatorCamera.png) @@ -347,11 +364,62 @@ Refer to script canvas example below: *Note* Only one gizmo can be rendered at the time! +# RandomizeUtils + +This gem allows to randomize prefab on spawning. +It has a component called `RandomizePoseComponent` that modifies an entity during activation. +It allows: + - change translation and rotation and uniform scale of the Transform component, + - deactivate the entity with given probability + +![](doc/RandomizePoseComponent.png) + +**Note:** that only given entity is modified (not all descendants). + +# ImGuiProvider + +This gem adds support for displaying user defined ImGui GUI. Users can define their own gui using `ImGuiProvider::ImGuiProviderNotificationBus`. User's component should be handler of the `ImGuiProvider::ImGuiProviderNotificationBus::Handler` and define method `OnImGuiUpdate`. Mentioned method should contain all code related to displayed GUI. Acquiring ImGui context and its releasing is handled by the Gem and its system component. User's component should connect to `ImGuiProvider::ImGuiProviderNotificationBus` using `ImGuiProvider::ImGuiFeaturePath` aka `AZ::IO::Path`. Each segment of path represents one depth in the toolbar. + +Below example on how to register new feature during component activation: + +```cpp +void ExampleComponent::Activate() +{ + /* + some implementation + */ + auto pathToFeature = ImGuiProvider::ImGuiFeaturePath{ "Tools/ExampleFeature" }; + ImGuiProvider::ImGuiProviderNotificationBus::Handler::BusConnect(pathToFeature); + /* + some implementation + */ +} +``` + +Gem monitors number of active handler, so ImGui features are available as long as the lifetime of component which registered it. + +Gem handles correct displaying of debug menu available using `home` button. If Gem detects debug menu, registered GUIs disappear. After disabling debug menu, previous state of registered features is restored. +If Gem detects custom GUI feature registered in Editor, viewport icons are moved to prevent covering the registered features. + +## API + +Gem defines `ImGuiProvider::ImGuiProviderNotificationBus` and `ImGuiProvider::ImGuiProviderRequestBus`. + +**Notification bus methods** + +Besides `OnImGuiUpdate` used for updating displayed GUI, notification bus defines methods: `OnImGuiSelected` and `OnImGuiUnselected`. Methods are triggered during registered GUI state change, respectively hidden -> visible and visible -> hidden. + +**Request bus methods** + +`AZStd::optional GetActiveGuiId()` - returns optional with ImGuiProvider::ImGuiFeaturePath. Optional is empty if there is no active GUI. + +`void SetActiveGUI(ImGuiFeaturePath guiId)` - sets GUI with given id as active. Doesn't check if given guiId exists. + # FPSProfiler This gem provides a tool to collect statistics in the game mode of the FPS, CPU and GPU into `csv` file. The Profiler has a EBus which can control profiling in runtime (start/stop/reset), save profiled data, change save path, access current frame memory data or fps (avg, min, max). -It is also provided with a set of notification functions, making it highly customizable for end user. +It is also provided with a set of notification functions, making it highly customizable for end user. This functionality can be accessed in c++, lua and script canvas. > *To start using the tool, add a `FPSProfiler` to the **Level** entity.* From 0ebc0a1b336c3d6ce28466a8c6fc26a65de27096 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Thu, 3 Apr 2025 15:58:00 +0200 Subject: [PATCH 171/175] readme update Signed-off-by: Wojciech Czerski --- readme.md | 45 ++++++++++++++++++--------------------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/readme.md b/readme.md index 9efd845e..ba8ccf25 100644 --- a/readme.md +++ b/readme.md @@ -427,37 +427,28 @@ This functionality can be accessed in c++, lua and script canvas. ## Component Functionality ![FpsProfiler Editor](doc/FpsProfiler.png) -### Save Settings -| File Save Settings | Description | -|----------------------|----------------------------------------------------------------------------------------------------------------| -| Select Csv File Path | Button that opens a File Dialog. | -| Csv Save Path | A path where *.csv will be saved. | -| Auto Save | Enables automatic saving of FPS logs per frame. | -| Auto Save At Frame | Specifies the frame interval for auto-saving. | -| Timestamp | Includes timestamps in the FPS log file name.
Allows to save automatically without manual input each time. | - -### Recording Settings -| Recording Settings | Description | +| Config | Description | |------------------------|----------------------------------------------------------------------------------------------------------------------| +| **File Save Settings** | | +| Select Csv File Path | Button that opens a File Dialog. | +| Csv Save Path | A path where *.csv will be saved. | +| Auto Save | Enables automatic saving of FPS logs per frame. | +| Auto Save At Frame | Specifies the frame interval for auto-saving. | +| Timestamp | Includes timestamps in the FPS log file name.
Allows to save automatically without manual input each time. | +| **Recording Settings** | | | Record Type | Specify when to start recording:
- at game start
- selected frame
- await for other system to call start | | Frames To Skip | Number of frames to skip before recording.
Only enabled when type is `SelectFrame`. | | Frames To Record | Total number of frames to record. If set to 0 - unlimited. | | Record Stats | Specifies what type of stats to record (FPS data, CPU and GPU). | - -### Precision Settings -| Precision Settings | Description | -|------------------------|------------------------------------------------------------------------------------------------------------| -| Near Zero Precision | Precision threshold for near-zero values. | -| Moving Average Type | Type of moving average used for smoothing average FPS:
- Simple
- Exponential | -| Alpha Smoothing Factor | Factor applied to control smoothing effect when `Exponential` enabled. | -| Keep History | Keeps a history of recorded FPS data or clear after every auto-save.
For better effect - keep enabled. | - -### Debug Settings -| Debug Settings | Description | -|------------------------|-----------------------------------------| -| Print Debug Info | Displays debug information in the logs. | -| Show FPS | Enables FPS display on screen. | -| Debug Color | Color used for debugging FPS display. | +| **Precision Settings** | | +| Near Zero Precision | Precision threshold for near-zero values. | +| Moving Average Type | Type of moving average used for smoothing average FPS:
- Simple
- Exponential | +| Alpha Smoothing Factor | Factor applied to control smoothing effect when `Exponential` enabled. | +| Keep History | Keeps a history of recorded FPS data or clear after every auto-save.
For better effect - keep enabled. | +| **Debug Settings** | | +| Print Debug Info | Displays debug information in the logs. | +| Show FPS | Enables FPS display on screen. | +| Debug Color | Color used for debugging FPS display. | ## API Access Example workflows how to access and use a Fps Profiler Events and Notifications. @@ -507,7 +498,7 @@ return FPSProfilerHandler ### In Script Canvas Example how to stop profiling after 60 seconds have passed in Script Canvas. -![FpsProfiler Editor](doc/FpsProfiler_ScriptCanvas.png) +![FpsProfiler Script Canvas](doc/FpsProfiler_ScriptCanvas.png) ## Csv Output File - Example | Frame | FrameTime | CurrentFPS | MinFPS | MaxFPS | AvgFPS | CpuMemoryUsed | CpuMemoryReserved | GpuMemoryUsed | GpuMemoryReserved | From 6c209c746baa1458beb5099c5b1dc998fa4e99a2 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Fri, 4 Apr 2025 11:21:53 +0200 Subject: [PATCH 172/175] remove pure vritual from notify bus Signed-off-by: Wojciech Czerski --- .../Code/Include/FPSProfiler/FPSProfilerBus.h | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h index c883605e..c26b9f24 100644 --- a/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -140,19 +140,25 @@ namespace FPSProfiler * @brief Called when a new file is created. * @param config File Save Settings Configuration. */ - virtual void OnFileCreated(Configs::FileSaveSettings& config) = 0; + virtual void OnFileCreated(Configs::FileSaveSettings& config) + { + } /** * @brief Called when an existing file is updated. * @param config File Save Settings Configuration. */ - virtual void OnFileUpdate(Configs::FileSaveSettings& config) = 0; + virtual void OnFileUpdate(Configs::FileSaveSettings& config) + { + } /** * @brief Called when a file is successfully saved. * @param config File Save Settings Configuration. */ - virtual void OnFileSaved(Configs::FileSaveSettings& config) = 0; + virtual void OnFileSaved(Configs::FileSaveSettings& config) + { + } /** * @brief Called when the profiling process starts. @@ -161,14 +167,18 @@ namespace FPSProfiler * @param debugConfig The configuration settings used for the debugging. */ virtual void OnProfileStart( - Configs::RecordSettings& recordConfig, Configs::PrecisionSettings& precisionConfig, Configs::DebugSettings& debugConfig) = 0; + Configs::RecordSettings& recordConfig, Configs::PrecisionSettings& precisionConfig, Configs::DebugSettings& debugConfig) + { + } /** * @brief Called when the profiling data is reset. * @param recordConfig The configuration settings used for the record session. * @param precisionConfig The configuration settings used for the precision. */ - virtual void OnProfileReset(Configs::RecordSettings& recordConfig, Configs::PrecisionSettings& precisionConfig) = 0; + virtual void OnProfileReset(Configs::RecordSettings& recordConfig, Configs::PrecisionSettings& precisionConfig) + { + } /** * @brief Called when the profiling process stops. @@ -181,7 +191,9 @@ namespace FPSProfiler Configs::FileSaveSettings& saveConfig, Configs::RecordSettings& recordConfig, Configs::PrecisionSettings& precisionConfig, - Configs::DebugSettings& debugConfig) = 0; + Configs::DebugSettings& debugConfig) + { + } }; class FPSProfilerNotificationBusTraits : public AZ::EBusTraits From 427dcd8470db08e7c16fa467fe796723514d3b01 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Fri, 4 Apr 2025 11:28:48 +0200 Subject: [PATCH 173/175] add GetRequiredComponentType list override Signed-off-by: Wojciech Czerski --- .../Code/Source/FPSProfilerModuleInterface.cpp | 9 +++++++++ .../FPSProfiler/Code/Source/FPSProfilerModuleInterface.h | 1 + .../Code/Source/Tools/FPSProfilerEditorModule.cpp | 9 +++++++++ 3 files changed, 19 insertions(+) diff --git a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp index b5049c81..182ac1f4 100644 --- a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp +++ b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp @@ -24,4 +24,13 @@ namespace FPSProfiler FPSProfilerComponent::CreateDescriptor(), }); } + + /** + * Add required SystemComponents to the SystemEntity. + * Non-SystemComponents should not be added here + */ + AZ::ComponentTypeList FPSProfilerModuleInterface::GetRequiredSystemComponents() const + { + return AZ::ComponentTypeList{}; + } } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.h b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.h index ee0f505a..beaced96 100644 --- a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.h +++ b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.h @@ -14,5 +14,6 @@ namespace FPSProfiler AZ_CLASS_ALLOCATOR_DECL FPSProfilerModuleInterface(); + AZ::ComponentTypeList GetRequiredSystemComponents() const override; }; } // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp index b17623c3..e4572563 100644 --- a/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp @@ -23,6 +23,15 @@ namespace FPSProfiler FPSProfilerComponent::CreateDescriptor(), }); } + + /** + * Add required SystemComponents to the SystemEntity. + * Non-SystemComponents should not be added here + */ + AZ::ComponentTypeList GetRequiredSystemComponents() const override + { + return AZ::ComponentTypeList{}; + } }; } // namespace FPSProfiler From 55879d4c01668fc040c83884d3414f2d147c5ba9 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Fri, 4 Apr 2025 11:32:27 +0200 Subject: [PATCH 174/175] mark path seleciton as read only Signed-off-by: Wojciech Czerski --- .../FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp index 77536d47..266cd965 100644 --- a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp +++ b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp @@ -28,6 +28,7 @@ namespace FPSProfiler::Configs "Csv Save Path", "Select a path where *.csv will be saved.") ->Attribute(AZ::Edit::Attributes::SourceAssetFilterPattern, "*.csv") + ->Attribute(AZ::Edit::Attributes::ReadOnly, true) // todo handle proper path selection, for now only read only ->DataElement( AZ::Edit::UIHandlers::Default, From 6a382abbed2cf40a931cf2ec2b2ac3f661a19b88 Mon Sep 17 00:00:00 2001 From: Wojciech Czerski Date: Fri, 4 Apr 2025 11:49:15 +0200 Subject: [PATCH 175/175] update path validation function Signed-off-by: Wojciech Czerski --- .../Source/Clients/FPSProfilerComponent.cpp | 49 +++++++++---------- .../Source/Clients/FPSProfilerComponent.h | 2 +- .../Source/FPSProfilerModuleInterface.cpp | 6 +-- 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp index 29cba3fb..a2a9dac9 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp @@ -120,14 +120,11 @@ namespace FPSProfiler void FPSProfilerComponent::Activate() { - if (!IsPathValid(m_configFile.m_OutputFilename)) + auto pathValidationOutcome = IsPathValid(m_configFile.m_OutputFilename); + if (!pathValidationOutcome.IsSuccess()) { m_configFile.m_OutputFilename = "@user@/fps_log.csv"; - AZ_Warning( - "FPSProfiler", - !m_configDebug.m_PrintDebugInfo, - "Invalid output file path. Using default: %s", - m_configFile.m_OutputFilename.c_str()); + AZ_Warning("FPSProfilerComponent::Activate", !m_configDebug.m_PrintDebugInfo, pathValidationOutcome.GetError().c_str()); } // Reserve log entries buffer size based on known auto save per frame @@ -326,8 +323,10 @@ namespace FPSProfiler void FPSProfilerComponent::ChangeSavePath(const AZ::IO::Path& newSavePath) { - if (!IsPathValid(newSavePath)) + auto pathValidationOutcome = IsPathValid(newSavePath); + if (!pathValidationOutcome.IsSuccess()) { + AZ_Warning("FPSProfilerComponent::ChangeSavePath", !m_configDebug.m_PrintDebugInfo, pathValidationOutcome.GetError().c_str()); return; } @@ -462,14 +461,11 @@ namespace FPSProfiler return; } - if (!IsPathValid(m_configFile.m_OutputFilename)) + auto pathValidationOutcome = IsPathValid(m_configFile.m_OutputFilename); + if (!pathValidationOutcome.IsSuccess()) { m_configFile.m_OutputFilename = "@user@/fps_log.csv"; - AZ_Warning( - "FPSProfiler", - !m_configDebug.m_PrintDebugInfo, - "Invalid output file path. Using default: %s", - m_configFile.m_OutputFilename.c_str()); + AZ_Warning("FPSProfilerComponent::CreateLogFile", !m_configDebug.m_PrintDebugInfo, pathValidationOutcome.GetError().c_str()); } // Apply Timestamp @@ -530,23 +526,26 @@ namespace FPSProfiler return static_cast(bytes) / (1024.0f * 1024.0f); } - bool FPSProfilerComponent::IsPathValid(const AZ::IO::Path& path) const + AZ::Outcome FPSProfilerComponent::IsPathValid(const AZ::IO::Path& path) const { AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); - if (path.empty() || !path.HasFilename() || !path.HasExtension() || !fileIO || !fileIO->ResolvePath(path)) - { - const char* reason = path.empty() ? "Path cannot be empty." - : !path.HasFilename() ? "Path must have a file at the end." - : !path.HasExtension() ? "Path must have a *.csv extension." - : !fileIO ? "Could not get a FileIO object. Try again." - : "Path is not registered or recognizable by O3DE FileIO System."; + if (!fileIO) + return AZ::Failure("Could not get a FileIO object. Try again."); - AZ_Warning("FPSProfiler::IsPathValid", !m_configDebug.m_PrintDebugInfo, "%s", reason); - return false; - } + if (!fileIO->ResolvePath(path)) + return AZ::Failure("Cannot resolve the path."); + + if (path.empty()) + return AZ::Failure("Path cannot be empty."); + + if (!path.HasFilename()) + return AZ::Failure("Path must have a file at the end."); + + if (!path.HasExtension() || path.Extension() != ".csv") + return AZ::Failure("Path must have a *.csv extension."); - return true; + return AZ::Success(true); } void FPSProfilerComponent::ShowFps() const diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h index c71a153a..b5af8beb 100644 --- a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h @@ -65,7 +65,7 @@ namespace FPSProfiler // Utility Functions void CalculateFpsData(const float& deltaTime); static float BytesToMB(AZStd::size_t bytes); - [[nodiscard]] bool IsPathValid(const AZ::IO::Path& path) const; + [[nodiscard]] AZ::Outcome IsPathValid(const AZ::IO::Path& path) const; // Debug Display void ShowFps() const; diff --git a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp index 182ac1f4..88c7dabd 100644 --- a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp +++ b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp @@ -26,9 +26,9 @@ namespace FPSProfiler } /** - * Add required SystemComponents to the SystemEntity. - * Non-SystemComponents should not be added here - */ + * Add required SystemComponents to the SystemEntity. + * Non-SystemComponents should not be added here + */ AZ::ComponentTypeList FPSProfilerModuleInterface::GetRequiredSystemComponents() const { return AZ::ComponentTypeList{};