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..4e341d09 --- /dev/null +++ b/Gems/FPSProfiler/Code/CMakeLists.txt @@ -0,0 +1,248 @@ + +# 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 + AZ::AzFramework +) + +# 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 + AZ::AzToolsFramework + Gem::Atom_RHI.Public +) + +# 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..c26b9f24 --- /dev/null +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerBus.h @@ -0,0 +1,265 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace FPSProfiler +{ + class FPSProfilerRequests + { + public: + AZ_RTTI(FPSProfilerRequests, FPSProfilerRequestsTypeId); + virtual ~FPSProfilerRequests() = default; + + /** + * @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; + + /** + * @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; + + /** + * @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; + + /** + * @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; + }; + + 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; + + // Notifications EBus (For Sending Events) + class FPSProfilerNotifications + { + public: + AZ_RTTI(FPSProfilerNotifications, FPSProfilerNotificationsTypeId); + virtual ~FPSProfilerNotifications() = default; + + /** + * @brief Called when a new file is created. + * @param config File Save Settings Configuration. + */ + 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) + { + } + + /** + * @brief Called when a file is successfully saved. + * @param config File Save Settings Configuration. + */ + virtual void OnFileSaved(Configs::FileSaveSettings& config) + { + } + + /** + * @brief Called when the profiling process starts. + * @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( + 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) + { + } + + /** + * @brief Called when the profiling process stops. + * @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( + Configs::FileSaveSettings& saveConfig, + Configs::RecordSettings& recordConfig, + Configs::PrecisionSettings& precisionConfig, + Configs::DebugSettings& debugConfig) + { + } + }; + + 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; + + 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(Configs::FileSaveSettings& config) override + { + Call(FN_OnFileCreated, config); + } + + void OnFileUpdate(Configs::FileSaveSettings& config) override + { + Call(FN_OnFileUpdate, config); + } + + void OnFileSaved(Configs::FileSaveSettings& config) override + { + Call(FN_OnFileSaved, config); + } + + void OnProfileStart( + Configs::RecordSettings& recordConfig, + Configs::PrecisionSettings& precisionConfig, + Configs::DebugSettings& debugConfig) override + { + Call(FN_OnProfileStart, recordConfig, precisionConfig, debugConfig); + } + + void OnProfileReset(Configs::RecordSettings& recordConfig, Configs::PrecisionSettings& precisionConfig) override + { + Call(FN_OnProfileReset, recordConfig, precisionConfig); + } + + void OnProfileStop( + Configs::FileSaveSettings& saveConfig, + Configs::RecordSettings& recordConfig, + Configs::PrecisionSettings& precisionConfig, + Configs::DebugSettings& debugConfig) override + { + Call(FN_OnProfileStop, saveConfig, recordConfig, precisionConfig, debugConfig); + } + }; + +} // 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..916e72b9 --- /dev/null +++ b/Gems/FPSProfiler/Code/Include/FPSProfiler/FPSProfilerTypeIds.h @@ -0,0 +1,27 @@ + +#pragma once + +namespace FPSProfiler +{ + // System Component TypeIds + inline constexpr const char* FPSProfilerComponentTypeId = "{B11CE88E-E1C5-404F-83D2-0D3850445A13}"; + + // Configs TypeIds + 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}"; + + // 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}"; + 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/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/FPSProfilerComponent.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp new file mode 100644 index 00000000..a2a9dac9 --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.cpp @@ -0,0 +1,568 @@ +#include "FPSProfilerComponent.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace FPSProfiler +{ + void FPSProfilerComponent::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_configFile", &FPSProfilerComponent::m_configFile) + ->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 + 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) + { + provided.push_back(AZ_CRC_CE("FPSProfilerService")); + } + + void FPSProfilerComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC_CE("FPSProfilerService")); + } + + FPSProfilerComponent::FPSProfilerComponent() + { + if (!FPSProfilerInterface::Get()) + { + FPSProfilerInterface::Register(this); + } + } + + FPSProfilerComponent::FPSProfilerComponent( + 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()) + { + FPSProfilerInterface::Register(this); + } + } + + FPSProfilerComponent::~FPSProfilerComponent() + { + if (FPSProfilerInterface::Get() == this) + { + FPSProfilerInterface::Unregister(this); + } + } + + void FPSProfilerComponent::Activate() + { + auto pathValidationOutcome = IsPathValid(m_configFile.m_OutputFilename); + if (!pathValidationOutcome.IsSuccess()) + { + m_configFile.m_OutputFilename = "@user@/fps_log.csv"; + AZ_Warning("FPSProfilerComponent::Activate", !m_configDebug.m_PrintDebugInfo, pathValidationOutcome.GetError().c_str()); + } + + // 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); + + 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]() + { + StartProfiling(); + }); + } + + AZ_Printf("FPS Profiler", "FPSProfiler activated."); + } + + void FPSProfilerComponent::Deactivate() + { + AZ::TickBus::Handler::BusDisconnect(); + + if (!m_configFile.m_AutoSave || m_configRecord.m_framesToRecord == 0 || m_logBuffer.size() < m_configFile.m_AutoSaveAtFrame) + { + WriteDataToFile(); + } + + // Notify - File Saved + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileSaved, m_configFile); + FPSProfilerRequestBus::Handler::BusDisconnect(); + } + + void FPSProfilerComponent::OnTick([[maybe_unused]] float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) + { + if (!m_isProfiling) + { + return; + } + + // Update FPS data + CalculateFpsData(deltaTime); + + if (m_configDebug.m_ShowFps) + { + ShowFps(); + } + + if (m_configRecord.m_recordType == Configs::RecordType::FramePick && m_frameCount <= m_configRecord.m_framesToSkip) + { + // Wait for selected frame + return; + } + + if (m_configRecord.m_framesToRecord != 0 && m_configRecord.m_framesToRecord == m_recordedFrameCount++) + { + StopProfiling(); + } + + if (!IsAnySaveOptionEnabled()) + { + return; + } + + // Initialize log entry buffer + char logEntry[MAX_LOG_BUFFER_LINE_SIZE]; + int logEntryLength = 0; + + // 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_configRecord.m_RecordStats & Configs::RecordStatistics::CPU) + { + auto [cpuUsed, cpuReserved] = GetCpuMemoryUsed(); + usedCpu = BytesToMB(cpuUsed); + reservedCpu = BytesToMB(cpuReserved); + } + + 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) + { + 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 + if (m_configFile.m_AutoSave && (m_frameCount % m_configFile.m_AutoSaveAtFrame == 0)) + { + WriteDataToFile(); + } + } + + int FPSProfilerComponent::GetTickOrder() + { + return AZ::TICK_GAME; + } + + void FPSProfilerComponent::StartProfiling() + { + if (m_isProfiling) + { + AZ_Warning("FPS Profiler", !m_configDebug.m_PrintDebugInfo, "Profiler already activated."); + return; + } + + m_isProfiling = true; + ResetProfilingData(); + CreateLogFile(); + + // Connect TickBus only if not already connected + if (!AZ::TickBus::Handler::BusIsConnected()) + { + AZ::TickBus::Handler::BusConnect(); + } + + // Notify - Profile Started + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileStart, m_configRecord, m_configPrecision, m_configDebug); + AZ_Printf("FPS Profiler", "Profiling started."); + } + + void FPSProfilerComponent::StopProfiling() + { + if (!m_isProfiling) + { + AZ_Warning("FPS Profiler", !m_configDebug.m_PrintDebugInfo, "Profiler already stopped."); + return; + } + + m_isProfiling = false; + + // Disconnect TickBus only if actually connected + if (AZ::TickBus::Handler::BusIsConnected()) + { + AZ::TickBus::Handler::BusDisconnect(); + } + SaveLogToFile(); + + // Notify - Profile Stopped + FPSProfilerNotificationBus::Broadcast( + &FPSProfilerNotifications::OnProfileStop, m_configFile, m_configRecord, m_configPrecision, m_configDebug); + AZ_Printf("FPS Profiler", "Profiling stopped."); + } + + 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; + m_avgFps = 0.0f; + m_currentFps = 0.0f; + m_totalFrameTime = 0.0f; + m_frameCount = 0; + m_recordedFrameCount = 0; + m_fpsSamples.clear(); + m_logBuffer.clear(); + + // Notify - Profile Reset + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnProfileReset, m_configRecord, m_configPrecision); + AZ_Printf("FPS Profiler", "Profiling data reseted."); + } + + bool FPSProfilerComponent::IsProfiling() const + { + return m_isProfiling; + } + + bool FPSProfilerComponent::IsAnySaveOptionEnabled() const + { + return m_configRecord.m_RecordStats != Configs::RecordStatistics::None; + } + + void FPSProfilerComponent::ChangeSavePath(const AZ::IO::Path& newSavePath) + { + auto pathValidationOutcome = IsPathValid(newSavePath); + if (!pathValidationOutcome.IsSuccess()) + { + AZ_Warning("FPSProfilerComponent::ChangeSavePath", !m_configDebug.m_PrintDebugInfo, pathValidationOutcome.GetError().c_str()); + return; + } + + m_configFile.m_OutputFilename = newSavePath; + AZ_Warning("FPS Profiler", !m_configDebug.m_PrintDebugInfo && !m_isProfiling, "Path changed during activated profiling."); + } + + void FPSProfilerComponent::SafeChangeSavePath(const AZ::IO::Path& newSavePath) + { + // If profiling is enabled, save current opened file and stop profiling. + StopProfiling(); + ChangeSavePath(newSavePath); + } + + float FPSProfilerComponent::GetMinFps() const + { + return m_minFps; + } + + float FPSProfilerComponent::GetMaxFps() const + { + return m_maxFps; + } + + float FPSProfilerComponent::GetAvgFps() const + { + return m_avgFps; + } + + float FPSProfilerComponent::GetCurrentFps() const + { + return m_currentFps; + } + + AZStd::pair FPSProfilerComponent::GetCpuMemoryUsed() const + { + AZStd::size_t usedBytes = 0; + AZStd::size_t reservedBytes = 0; + + AZ::AllocatorManager::Instance().GetAllocatorStats(usedBytes, reservedBytes); + return { usedBytes, reservedBytes }; + } + + AZStd::pair FPSProfilerComponent::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::Basic); + + return { memoryStats.m_heaps.front().m_memoryUsage.m_totalResidentInBytes, + memoryStats.m_heaps.front().m_memoryUsage.m_budgetInBytes }; + } + } + + return { 0, 0 }; + } + + void FPSProfilerComponent::SaveLogToFile() + { + WriteDataToFile(); + } + + void FPSProfilerComponent::SaveLogToFileWithNewPath(const AZ::IO::Path& newSavePath, bool useSafeChangePath) + { + if (useSafeChangePath) + { + SafeChangeSavePath(newSavePath); + } + else + { + ChangeSavePath(newSavePath); + } + + WriteDataToFile(); + } + + void FPSProfilerComponent::ShowFpsOnScreen(bool enable) + { + m_configDebug.m_ShowFps = enable; + } + + void FPSProfilerComponent::CalculateFpsData(const float& deltaTime) + { + m_currentFps = deltaTime > 0 ? (1.0f / deltaTime) : 0.0f; + m_totalFrameTime += deltaTime; + m_frameCount++; + + // Latest fps history for avg fps calculation + m_fpsSamples.push_back(m_currentFps); + if (!m_configFile.m_AutoSave && !m_configPrecision.m_keepHistory && m_fpsSamples.size() > m_configFile.m_AutoSaveAtFrame) + { + m_fpsSamples.pop_front(); + } + + 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) + { + m_minFps = AZStd::min(m_minFps, m_currentFps); + } + + m_maxFps = AZStd::max(m_maxFps, m_currentFps); + } + + void FPSProfilerComponent::CreateLogFile() + { + if (!IsAnySaveOptionEnabled()) + { + AZ_Warning("FPSProfiler", !m_configDebug.m_PrintDebugInfo, "None save option selected. Skipping file creation."); + return; + } + + auto pathValidationOutcome = IsPathValid(m_configFile.m_OutputFilename); + if (!pathValidationOutcome.IsSuccess()) + { + m_configFile.m_OutputFilename = "@user@/fps_log.csv"; + AZ_Warning("FPSProfilerComponent::CreateLogFile", !m_configDebug.m_PrintDebugInfo, pathValidationOutcome.GetError().c_str()); + } + + // Apply Timestamp + 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); + + std::tm timeInfo{}; + localtime_r(&now_time_t, &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()) + .data()); + } + + // Write profiling headers to file + 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_configFile); + } + + void FPSProfilerComponent::WriteDataToFile() + { + if (m_logBuffer.empty()) + { + return; + } + + if (!IsAnySaveOptionEnabled()) + { + return; + } + + AZ::IO::HandleType 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); + } + m_logBuffer.clear(); + + // Notify - File Update + FPSProfilerNotificationBus::Broadcast(&FPSProfilerNotifications::OnFileUpdate, m_configFile); + } + + float FPSProfilerComponent::BytesToMB(AZStd::size_t bytes) + { + return static_cast(bytes) / (1024.0f * 1024.0f); + } + + AZ::Outcome FPSProfilerComponent::IsPathValid(const AZ::IO::Path& path) const + { + AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance(); + + if (!fileIO) + return AZ::Failure("Could not get a FileIO object. Try again."); + + 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 AZ::Success(true); + } + + void FPSProfilerComponent::ShowFps() const + { + AzFramework::DebugDisplayRequestBus::BusPtr debugDisplayBus; + AzFramework::DebugDisplayRequestBus::Bind(debugDisplayBus, AzFramework::g_defaultSceneEntityDebugDisplayId); + AzFramework::DebugDisplayRequests* debugDisplay = AzFramework::DebugDisplayRequestBus::FindFirstHandler(debugDisplayBus); + + if (!debugDisplay) + { + return; + } + + debugDisplay->SetColor(AZ::Colors::Red); + debugDisplay->SetAlpha(1.0f); + + AZStd::string debugText = AZStd::string::format("FPS: %.2f", m_currentFps); + debugDisplay->Draw2dTextLabel(100, 50, 1.0f, debugText.c_str(), true); + } +} // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h new file mode 100644 index 00000000..b5af8beb --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerComponent.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace FPSProfiler +{ + class FPSProfilerComponent final + : public AZ::Component + , protected AZ::TickBus::Handler + , protected FPSProfilerRequestBus::Handler + { + public: + AZ_COMPONENT(FPSProfilerComponent, FPSProfilerComponentTypeId, Component); + + static void Reflect(AZ::ReflectContext* context); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + + FPSProfilerComponent(); + explicit FPSProfilerComponent( + const Configs::FileSaveSettings& configF, + const Configs::RecordSettings& configS, + const Configs::PrecisionSettings& configP, + const Configs::DebugSettings& configD); + ~FPSProfilerComponent() override; + + // 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; + + // FPSProfilerRequestBus::Handler implementation + void StartProfiling() override; + void StopProfiling() override; + 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; + [[nodiscard]] float GetMinFps() const override; + [[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; + void SaveLogToFile() override; + void SaveLogToFileWithNewPath(const AZ::IO::Path& newSavePath, bool useSafeChangePath) override; + void ShowFpsOnScreen(bool enable) override; + + private: + // File Operations + void CreateLogFile(); + void WriteDataToFile(); + + // Utility Functions + void CalculateFpsData(const float& deltaTime); + static float BytesToMB(AZStd::size_t bytes); + [[nodiscard]] AZ::Outcome 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 + + // 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 + 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. Used when @ref m_configRecord.m_framesToRecord != 0 + 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 + }; +} // namespace FPSProfiler diff --git a/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerModule.cpp b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerModule.cpp new file mode 100644 index 00000000..51b61564 --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/Clients/FPSProfilerModule.cpp @@ -0,0 +1,20 @@ + +#include "FPSProfilerComponent.h" +#include +#include + +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/Configurations/FPSProfilerConfig.cpp b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp new file mode 100644 index 00000000..266cd965 --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.cpp @@ -0,0 +1,280 @@ +#include "FPSProfilerConfig.h" + +#include +#include + +namespace FPSProfiler::Configs +{ + void FileSaveSettings::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ->Field("m_OutputFilename", &FileSaveSettings::m_OutputFilename) + ->Field("m_AutoSave", &FileSaveSettings::m_AutoSave) + ->Field("m_AutoSaveAtFrame", &FileSaveSettings::m_AutoSaveAtFrame) + ->Field("m_SaveWithTimestamp", &FileSaveSettings::m_SaveWithTimestamp); + + if (AZ::EditContext* editContext = serializeContext->GetEditContext()) + { + 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( + AZ::Edit::UIHandlers::Default, + &FileSaveSettings::m_OutputFilename, + "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, + &FileSaveSettings::m_AutoSave, + "Auto Save", + "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( + AZ::Edit::UIHandlers::Default, + &FileSaveSettings::m_AutoSaveAtFrame, + "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 FileSaveSettings* data = static_cast(instance); + return data && data->m_AutoSave ? AZ::Edit::PropertyVisibility::Show : AZ::Edit::PropertyVisibility::Hide; + }) + + ->DataElement( + AZ::Edit::UIHandlers::Default, + &FileSaveSettings::m_SaveWithTimestamp, + "Timestamp", + "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."); + } + } + + if (auto behaviorContext = azrtti_cast(context)) + { + if (behaviorContext->m_classes.contains("FileSaveSettings")) + { + return; + } + + behaviorContext->Class("FileSaveSettings") + ->Attribute(AZ::Script::Attributes::Category, "FPSProfiler") + ->Constructor<>() + ->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)); + } + } + + 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("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( + AZ::Edit::UIHandlers::ComboBox, &RecordSettings::m_recordType, "Record Type", "Specifies the type of record.") + ->EnumAttribute(RecordType::GameStart, "Game Start") + ->EnumAttribute(RecordType::FramePick, "Frame Pick") + ->EnumAttribute(RecordType::Await, "Await") + ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ::Edit::PropertyRefreshLevels::EntireTree) + + ->DataElement( + AZ::Edit::UIHandlers::Default, + &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; + }) + ->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) + ->Attribute(AZ::Edit::Attributes::Step, 100) + + ->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); + } + } + + if (auto behaviorContext = azrtti_cast(context)) + { + behaviorContext->Class("RecordSettings") + ->Attribute(AZ::Script::Attributes::Category, "FPSProfiler") + ->Constructor<>() + ->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)); + } + } + + void PrecisionSettings::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ->Field("m_NearZeroPrecision", &PrecisionSettings::m_NearZeroPrecision) + ->Field("m_avgFpsType", &PrecisionSettings::m_avgFpsType) + ->Field("m_smoothingFactor", &PrecisionSettings::m_smoothingFactor) + ->Field("m_keepHistory", &PrecisionSettings::m_keepHistory); + + if (auto* editContext = serializeContext->GetEditContext()) + { + 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, + "Moving Average Type", + "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, + "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::Default, + &PrecisionSettings::m_keepHistory, + "Keep History", + "Enabled saves entire history for better avg fps smoothing, otherwise history is cleared per auto save if " + "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("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)); + } + } + + void DebugSettings::Reflect(AZ::ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(0) + ->Field("m_PrintDebugInfo", &DebugSettings::m_PrintDebugInfo) + ->Field("m_ShowFps", &DebugSettings::m_ShowFps) + ->Field("m_Color", &DebugSettings::m_Color); + + if (auto* editContext = serializeContext->GetEditContext()) + { + 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.") + ->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.") + ->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; + }); + } + } + + if (auto* behaviorContext = azrtti_cast(context)) + { + if (behaviorContext->m_classes.contains("DebugSettings")) + { + return; + } + + behaviorContext->Class("DebugSettings") + ->Attribute(AZ::Script::Attributes::Category, "FPSProfiler") + ->Constructor<>() + ->Property("m_PrintDebugInfo", BehaviorValueProperty(&DebugSettings::m_PrintDebugInfo)) + ->Property("m_ShowFps", BehaviorValueProperty(&DebugSettings::m_ShowFps)) + ->Property("m_Color", BehaviorValueProperty(&DebugSettings::m_Color)); + } + } +} // namespace FPSProfiler::Configs diff --git a/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.h b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.h new file mode 100644 index 00000000..aa5b4643 --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/Configurations/FPSProfilerConfig.h @@ -0,0 +1,81 @@ +#pragma once + +#include + +#include +#include +#include + +namespace FPSProfiler::Configs +{ + enum MovingAverageType + { + Simple = 0, + Exponential = 1, + }; + AZ_DEFINE_ENUM_RELATIONAL_OPERATORS(MovingAverageType); + + enum RecordType : uint8_t + { + GameStart = 0, + 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, + MemoryUsage = CPU | GPU, + }; + AZ_DEFINE_ENUM_BITWISE_OPERATORS(RecordStatistics); + AZ_DEFINE_ENUM_RELATIONAL_OPERATORS(RecordStatistics); + + 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; + }; + + struct RecordSettings + { + AZ_TYPE_INFO(RecordSettings, FPSProfilerConfigRecordTypeId); + static void Reflect(AZ::ReflectContext* context); + + RecordType m_recordType = RecordType::GameStart; + int m_framesToSkip = 0; // Available only for FramePick + int m_framesToRecord = 0; + RecordStatistics m_RecordStats = RecordStatistics::All; + }; + + struct PrecisionSettings + { + AZ_TYPE_INFO(PrecisionSettings, FPSProfilerConfigPrecisionTypeId); + static void Reflect(AZ::ReflectContext* context); + + float m_NearZeroPrecision = 0.01f; + MovingAverageType m_avgFpsType = MovingAverageType::Simple; + float m_smoothingFactor = 2.0f; + bool m_keepHistory = false; + }; + + 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::DarkRed; + }; + +} // namespace FPSProfiler::Configs diff --git a/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp new file mode 100644 index 00000000..88c7dabd --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.cpp @@ -0,0 +1,36 @@ + +#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 SerializeContext, BehaviorContext and EditContext. + // This happens through the [MyComponent]::Reflect() function. + m_descriptors.insert( + m_descriptors.end(), + { + 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 new file mode 100644 index 00000000..beaced96 --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/FPSProfilerModuleInterface.h @@ -0,0 +1,19 @@ + +#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(); + 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..e4572563 --- /dev/null +++ b/Gems/FPSProfiler/Code/Source/Tools/FPSProfilerEditorModule.cpp @@ -0,0 +1,42 @@ +#include "Clients/FPSProfilerComponent.h" + +#include +#include + +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(), + { + 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 + +#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/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..f5526eeb --- /dev/null +++ b/Gems/FPSProfiler/Code/fpsprofiler_editor_private_files.cmake @@ -0,0 +1,3 @@ + +set(FILES +) 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..f5526eeb --- /dev/null +++ b/Gems/FPSProfiler/Code/fpsprofiler_editor_tests_files.cmake @@ -0,0 +1,3 @@ + +set(FILES +) diff --git a/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake b/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake new file mode 100644 index 00000000..b42e63b2 --- /dev/null +++ b/Gems/FPSProfiler/Code/fpsprofiler_private_files.cmake @@ -0,0 +1,9 @@ + +set(FILES + Source/FPSProfilerModuleInterface.cpp + Source/FPSProfilerModuleInterface.h + Source/Clients/FPSProfilerComponent.cpp + Source/Clients/FPSProfilerComponent.h + Source/Configurations/FPSProfilerConfig.cpp + Source/Configurations/FPSProfilerConfig.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..bca95a2a --- /dev/null +++ b/Gems/FPSProfiler/gem.json @@ -0,0 +1,30 @@ +{ + "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": "RobotecAI", + "origin_url": "TBD", + "type": "Code", + "summary": "Allows profiling FPS, CPU, GPU data and collect statistics into csv file", + "canonical_tags": [ + "Gem" + ], + "user_tags": [ + "FPSProfiler" + ], + "platforms": [ + "" + ], + "icon_path": "preview.png", + "requirements": "Requires Atom RHI dependency!", + "documentation_url": "Refer to the documentation available at repository.", + "dependencies": [ + "Atom_RHI" + ], + "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 00000000..83afae48 Binary files /dev/null and b/Gems/FPSProfiler/preview.png differ diff --git a/doc/FpsProfiler.png b/doc/FpsProfiler.png new file mode 100644 index 00000000..7a41c787 Binary files /dev/null and b/doc/FpsProfiler.png differ diff --git a/doc/FpsProfiler_ScriptCanvas.png b/doc/FpsProfiler_ScriptCanvas.png new file mode 100644 index 00000000..bd854ba0 Binary files /dev/null and b/doc/FpsProfiler_ScriptCanvas.png differ diff --git a/readme.md b/readme.md index 28a4ec5e..ba8ccf25 100644 --- a/readme.md +++ b/readme.md @@ -414,3 +414,97 @@ Besides `OnImGuiUpdate` used for updating displayed GUI, notification bus define `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. +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) + +| 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** | | +| 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. + +### In C++: +```c++ +// Example with Interface +auto profiler = FPSProfiler::FPSProfilerInterface::Get(); +if (!profiler) +{ + return; +} + +profiler->StartProfiling(); +float currentFps = profiler->GetCurrentFps(); + +// 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 + +void OnProfileStart(const Configs::FileSaveSettings& config) override +{ + // Your logic ... +} +``` + +### In Lua +```lua +FPSProfilerHandler = {} + +-- Called when a new file is created +function FPSProfilerHandler:OnFileCreated(config) + Debug.Log("File Created: " .. config.m_OutputFilename) +end + +function FPSProfilerHandler:OnActivate() + -- Connect the handler to listen for notifications + FPSProfilerNotificationBus.Connect(FPSProfilerHandler) +end + +return FPSProfilerHandler +``` + +### In Script Canvas +Example how to stop profiling after 60 seconds have passed in Script Canvas. +![FpsProfiler Script Canvas](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 |